tcc-grant-audit

v2026.09.24

Audit and clean macOS TCC privacy grants (Accessibility, Screen Recording, Camera, Microphone, Full Disk Access, AppleEvents, folder access) across ALL ~40 services and all three permission stores — not just the two System Settings shows. Clears grants stranded by uninstalled apps, which Apple's tccutil cannot reach and macOS never garbage-collects. Use when auditing privacy permissions, removing remote-access or spyware-adjacent software, investigating what can see the screen or drive the keyboard, or when tccutil fails with -10814 kLSApplicationNotFoundErr. TRIGGERS - TCC, tccutil, privacy permissions, Accessibility permission, Screen Recording permission, Full Disk Access, orphaned permission, -10814, kLSApplicationNotFoundErr, revoke permission, TCC.db, permission audit, what can record my screen, remove remote access software.

GitHub
安装命令
npx skhub add terrylica/tcc-grant-audit
Markdown
SKILL.md

tcc-grant-audit — macOS privacy grants, all stores

Self-Evolving skill — every rule below was MEASURED, not inferred. If a macOS release changes one, fix this file and the scripts; see Post-Execution Reflection.

Resolve the scripts against the version Claude Code actually loaded:

SKILL="$(cc-plugin-root macos-permissions)/skills/tcc-grant-audit"

Start here, always

bash "$SKILL/tcc-audit.sh" all        # READ-ONLY. Changes nothing.

🔴 The seven things that will bite you

1. tccutil is not the only tool, and is the wrong one for half the job. Reach for the whole set:

NeedTool
Query any grant, any service, any storesqlite3 -readonly on TCC.db
Is this bundle id still installed?NSWorkspace via osascript -l JavaScript (one line)
What does a grant actually pin?/usr/bin/csreq -r- -t
Watch grants change livesudo eslogger tcc_modify
Ground truth on whether a write landedlog show --predicate 'subsystem == "com.apple.TCC"'
Reset a grant for an INSTALLED app/usr/bin/tccutil reset <Service> <id>
Clear a grant for an UNINSTALLED apptcc-clear-orphans.sh (stub technique)
Remove a client_type=1 path rowa direct DB writer, user store only

2. Order is load-bearing: reset the grant BEFORE deleting the app. tccutil resolves the identifier through LaunchServices first. Delete the app and every reset fails -10814 forever.

3. tccutil reports success on a no-op. It prints Successfully reset and exits 0 while deleting nothing. Verify every change with SELECT COUNT(*) against the database. Third-party tools do this too — a direct DB writer exits 0 when the write was refused.

3b. Verifying immediately is not verifying. A daemon that owns a file holds it in memory and flushes over your edit minutes later. Measured: a PlistBuddy prune of the Sequoia store verified clean, and roughly eight minutes later replayd rewrote every deleted entry back from its cached dictionary. The first verification passed only because it read the file before the flush.

So, for any store owned by a live daemon:

killall replayd            # launchd restarts it on demand, re-reading from disk
sleep 2
# ...make the edit...
sleep 20                   # then RE-verify; an immediate read proves only that
                           # the write landed, not that it survived

Generalise the rule: verify the authority, not the artifact, and verify after it has had a chance to disagree with you.

4. There are THREE stores, with decreasing visibility.

StorePathPrivilegeHolds
system/Library/Application Support/com.apple.TCC/TCC.dbrootAccessibility, ScreenCapture, SystemPolicyAllFiles, ListenEvent, PostEvent
user~/Library/Application Support/com.apple.TCC/TCC.dbconsole userCamera, Microphone, AppleEvents, folder access, Photos
Sequoia~/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plistconsole userscreen-capture approvals; no CLI, no UI, tccutil cannot address it

Running a user-store operation under sudo targets root's store and reports success. Both directions of this mistake are silent.

5. Do not let System Settings frame the audit. Accessibility and Screen Recording have visible panes; Camera, Microphone and Full Disk Access do not surface orphans at all. In a real cleanup only 2 of ~40 services were addressed, and the large majority of orphaned grants remained — including Camera and Microphone still granted to uninstalled remote-access tools, and several orphaned Full Disk Access grants. Enumerate from the database.

6. csreq pins the SIGNING IDENTITY, not the bundle name. Decode it and see:

sqlite3 -readonly "/Library/Application Support/com.apple.TCC/TCC.db" \
  "SELECT hex(csreq) FROM access WHERE client='com.example.app' LIMIT 1;" | xxd -r -p | /usr/bin/csreq -r- -t

So malware renaming itself to an orphaned identifier inherits nothing. State this accurately: orphaned rows are inert; the real (narrow) exposure is that reinstalling the genuine, same-signed app silently regains the grant with no prompt. Do not oversell the threat — and note that live grants carry identical exposure and usually outnumber the dead ones.

7. com.apple.* entries are resolver false positives. They are XPC services, not apps, so NSWorkspace cannot find them and they look orphaned — typically dozens per user store. Never clear them; you can break iCloud, Reminders, Passwords or Find My.

Clearing orphans

bash      "$SKILL/tcc-clear-orphans.sh" preview user
bash      "$SKILL/tcc-clear-orphans.sh" preview system
bash      "$SKILL/tcc-clear-orphans.sh" user          # NOT sudo
sudo bash "$SKILL/tcc-clear-orphans.sh" system        # sudo

The mechanism: stage one throwaway .app in /Applications carrying the orphaned identifier, register it, tccutil reset All <id>, unregister, delete.

🔴 The stub MUST be in /Applications. Measured with all other variables held constant: a stub in /private/tmp is registered (it appears in lsregister -dump) and tccutil still returns -10814 — with a 0-byte executable, a real Mach-O, or ad-hoc signed. tccutil does not read the registration database; it calls an application-lookup API that only returns bundles from standard application directories. Registration is not resolvability.

Path-type rows (client_type=1)

tccutil accepts identifiers only, so these are unreachable by it at any scope. A direct TCC.db writer (e.g. jacobsalmela/tccutil, GPL-2.0) handles them — but the SIP boundary is per-database, measured: writes succeed against the user store with SIP enabled and are refused by the system store ("you probably need to disable SIP"). Do not disable SIP to delete inert rows.

⚠️ If you install that tool via Homebrew it lands as tccutil and shadows /usr/bin/tccutil, silently changing the meaning of every tccutil in your scripts. Keep it brew unlinked, expose it under a distinct name, and always call Apple's by absolute path /usr/bin/tccutil.

Before you delete an application

  1. Reset its TCC grants now, while the bundle still resolves.
  2. Check launchd persistence by the Label inside the plist, never the filename.
  3. Then remove files.

Reporting results

State separately: what was cleared, what was deliberately skipped and why (com.apple.*, path rows, live apps), and what the change is actually worth. If the honest answer is "housekeeping, not a security improvement," say so — then point at where the real surface is (live grants, listening sockets, the firewall).

Post-Execution Reflection

  1. Did a verification disagree with a tool's exit code? That is the normal case — make sure the script reported the database, not the tool.
  2. Did a resolver call return NOT_INSTALLED for something that exists? Find the class (non-app bundle, helper outside /Applications) and add it to rule 7.
  3. Did a macOS update move a store or change tccutil's surface? Update the store table.
  4. Did you clear rows whose real-world benefit was nil? Record that honestly rather than reporting a count as an achievement.
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

2026年9月24日

分类

未分类

许可证

MIT

源路径

plugins/macos-permissions/skills/tcc-grant-audit

默认分支

main

最新提交

b657cca

Tree SHA

906e003