TCC.db Forensics: The Investigator's Guide
What the macOS TCC database records, what it proves and what it does not, the services that matter, and a repeatable workflow for reviewing TCC.db in a case.
TL;DR. Transparency, Consent and Control (TCC) is the macOS framework that decides whether a program may use the camera, microphone, screen, input devices, accessibility APIs, protected folders, the whole disk, or other apps through Apple Events. The daemon tccd stores each decision as a row in a SQLite database, TCC.db: one for the system and one per user. Each row tells you which client, which service, allowed or denied, why, and when the row last changed. It is a permissions register, not a usage log. TCC Parser decodes these databases in your browser, including earlier row versions left in the WAL file; nothing is uploaded.
This guide is the entry point to the series. Each section links to a deeper article.
What TCC is and why investigators care
Most interesting things malware or an intruder wants to do on a Mac touch a TCC-protected resource: reading Mail, Messages or browser data (needs Full Disk Access), watching the screen (Screen Recording), logging keys (Input Monitoring), clicking through dialogs (Accessibility), or driving Finder and System Events to act on the user's behalf (Apple Events automation). When a program asks for one of those capabilities, tccd either prompts the user, applies a policy, or refuses, and records the outcome.
That makes TCC.db one of the places where intent becomes visible. A path-based helper in a hidden cache folder asking for Accessibility, or Terminal getting Full Disk Access minutes after a suspicious download, is the kind of thing the database captures well.
What a row can prove, and what it cannot
| A row can show | A row cannot show |
|---|---|
| That a client (bundle ID or absolute path) holds, or was refused, a service | That the capability was actually used, or how often |
| Whether the decision was allowed, denied, unknown or limited (auth_value) | What was captured, read or typed |
| A reason code for the decision (auth_reason, community-documented) | Who was physically at the keyboard |
| The code-signing requirement the client must satisfy (csreq) | That the binary on disk today is the one that was approved |
| For Automation, which app is being controlled | Every earlier state of the row (only the latest, unless the WAL keeps more) |
When the row last changed (last_modified, Unix seconds UTC) | When the permission was first granted, if the row changed since |
Two consequences shape every report. First, a grant is capability, not activity: write "held Full Disk Access from 10:11:37 UTC", not "read the user's mail". Second, absence is weak evidence: rows get deleted by tccutil reset, by resets in System Settings and when apps are removed. See recovering removed grants from the WAL.
Where the databases live
| Database | Path | Typical content |
|---|---|---|
| System | /Library/Application Support/com.apple.TCC/TCC.db | Full Disk Access, Accessibility, Screen Recording, Input Monitoring, Developer Tools, Endpoint Security clients |
| Per user | ~/Library/Application Support/com.apple.TCC/TCC.db | Camera, microphone, contacts, calendars, photos, Automation, Desktop/Documents/Downloads, removable and network volumes |
| MDM grants | /Library/Application Support/com.apple.TCC/MDMOverrides.plist | PPPC grants pushed by a management server |
The "typical content" column is a guide: the split is by service and has moved between releases, so always query every copy. Reading any of these files on a live Mac needs Full Disk Access for the process doing the copy, and System Integrity Protection additionally protects the system database against modification, even by root. The companion files TCC.db-wal and TCC.db-shm matter too. Collection details and tool gotchas: TCC.db location and acquisition.
Reading a row
The table that matters is access. On Big Sur and later, the columns you will use most are:
| Column | What to read from it |
|---|---|
service | The TCC service, e.g. kTCCServiceSystemPolicyAllFiles |
client | Bundle ID or absolute path of the program |
client_type | 0 bundle ID, 1 absolute path (client_type) |
auth_value | 0 denied, 1 unknown, 2 allowed, 3 limited |
auth_reason | Why (user consent, user set, MDM policy, and so on; community-documented) |
csreq | Compiled code requirement identifying the client |
indirect_object_identifier | For Automation: the app being controlled |
last_modified | Unix epoch seconds, UTC |
Mojave and Catalina used allowed and prompt_count instead of auth_value and auth_reason; Sonoma added pid, pid_version, boot_uuid and last_reminded. Always check .schema access on your copy. The full evolution, and the smaller tables around access, are in the access table across macOS versions.
The services that matter most
| Service identifier | Shown in System Settings as | Why it matters |
|---|---|---|
kTCCServiceSystemPolicyAllFiles | Full Disk Access | Reads protected user data: Mail, Messages, Safari, other TCC.db files |
kTCCServiceAccessibility | Accessibility | Drives the UI: clicks, keystrokes, approving dialogs |
kTCCServiceScreenCapture | Screen Recording | Sees everything on screen |
kTCCServiceListenEvent | Input Monitoring | Observes keyboard and mouse events |
kTCCServicePostEvent | (synthetic input) | Posts input events |
kTCCServiceAppleEvents | Automation | Controls another app; target in indirect_object_identifier |
kTCCServiceSystemPolicyDocumentsFolder and siblings | Files and Folders | Desktop, Documents, Downloads |
kTCCServiceSystemPolicyRemovableVolumes | Removable Volumes | USB drives and similar |
kTCCServiceEndpointSecurityClient | (Endpoint Security) | Security agents; a new one deserves a look |
Who decided: user, system or MDM
auth_reason is how you tell a user click from a policy. Commonly cited values include 2 user consent (answered a prompt), 3 user set (toggled in System Settings), 4 system set and 6 MDM policy. Apple does not document these integers; treat them as hypotheses and confirm on a test Mac of the same version or in the unified log before relying on them.
On managed Macs, grants pushed by a Privacy Preferences Policy Control (PPPC) profile are reflected in MDMOverrides.plist, and rows may reference the policies table through policy_id. A legitimate EDR agent with Full Disk Access from MDM is normal. An unexpected PPPC profile is itself a finding.
A repeatable workflow
- Collect the whole
com.apple.TCCfolder for the system and every user, with-waland-shm, from a process that holds Full Disk Access (or from a mounted image). Hash what you collected. - Check the schema of each database before querying: column sets differ by release.
- Review high-impact grants: Full Disk Access, Accessibility, Screen Recording, Input Monitoring, Post Event, Developer Tools, Endpoint Security. Ask who holds them and whether that fits the user's role.
- Look at path-based clients (
client_type = 1) and clients in temporary, shared or hidden folders. Legitimate apps are almost always bundle IDs. - Decode
csreq. A requirement that is only acdhashpins one exact build, which is typical of ad-hoc signed code. See decoding csreq. - Read Automation rows in each user database: which app may control Finder, System Events or Terminal.
- Recover history from
TCC.db-waland freed pages: removed and changed grants. - Build a timeline from
last_modifiedand correlate with the unified log (subsystem == "com.apple.TCC"), quarantine events, launchd jobs and shell history.
The permission abuse investigation article walks through this on a realistic, fictional intrusion.
Tools
- sqlite3 on a copy, with your own queries. Fine for spot checks; you decode blobs and reason codes yourself.
- mac_apt
TCCplugin: supports the Catalina and Big Sur-and-later layouts, from an image or in artifact-only mode. - Plaso
macostccplugin: expects the pre-Big Surallowed/prompt_countcolumns, so it targets Mojave and Catalina databases. - Velociraptor
MacOS.System.TCC: live queries across a fleet. - TCC Parser: drop the folder or a triage ZIP in the browser; it decodes every schema version, the code requirements and reason codes, flags notable rows, recovers WAL history and exports CSV, Timesketch CSV and JSON. The step-by-step is in analyze TCC.db in your browser.
Apple and macOS are trademarks of Apple Inc. TCC Parser is an independent tool and is not affiliated with or endorsed by Apple.
Pitfalls
- Incomplete collection. Without Full Disk Access the copy fails or comes back empty. A missing TCC.db in a triage package is usually a collection problem.
- Forgetting the WAL. Several collectors copy only
TCC.db. Recent changes may still sit inTCC.db-wal. - Responsible process. Permissions are often attributed to the app that launched a tool (Terminal) rather than the script that used them. A grant to Terminal covers everything run from it.
- Bundle ID versus binary. A bundle-ID row is honoured for any code that satisfies the stored requirement. Compare the requirement with the binary on disk.
- Over-reading reason codes.
auth_reasonmeanings are community-documented, not official.
FAQ
Does TCC.db show every time an app used the camera or read my files?
No. TCC.db stores standing decisions: which client was allowed or denied which service, and when that decision last changed. Individual uses are not recorded there. Per-request evidence is in the unified log under the com.apple.TCC subsystem, and usage history in other artifacts.
Is a missing row proof that a permission was never granted?
No. tccutil reset, a reset in System Settings or removal of the app deletes rows. Earlier versions of a row can survive for a while in TCC.db-wal or in freed pages, and older copies of the database may exist in APFS snapshots or Time Machine backups.
Which TCC database should I look at, the system one or the user one?
Both, every time. High-impact services such as Full Disk Access, Accessibility and Screen Recording are usually in the system database, while camera, microphone, Automation and folder access are usually in each user's database. The split is by service and has moved between releases, so query all copies.
What time zone is last_modified in?
last_modified is Unix epoch seconds, which is UTC. It is not Mac absolute time. The same applies to last_reminded (Sonoma and later) and expired_at in the expired table.