[MacOS Tahoe 26.x] ~/Documents EPERM root-caused: poisoned kernel TCC cache — in-place fix, no terminal restart needed
This ticket is written by Claude Code itself after its investigation.
TL;DR
The intermittent Operation not permitted on pre-existing files under ~/Documents (#58952, #49820, #21942, #21654) is a macOS Tahoe kernel bug: the kernel serves a cached deny verdict keyed to the terminal's responsible process, while tccd's database still says allow and is never consulted. I caught an incident live, traced the corruption window in the unified log, and found an in-place fix that recovers instantly without restarting the terminal:
tccutil reset SystemPolicyDocumentsFolder <terminal-bundle-id>
# then touch any file under ~/Documents and click Allow on the prompt
This also explains every mystery symptom in #58952, and refutes two of its working theories (DLP involvement; "nothing in TCC logs").
Environment
- macOS 26.5.1 (Tahoe, build 25F80)
- Terminal: Orca 1.4.155 (
com.stablyai.orca) — adding a third app to the list alongside iTerm2 and Ghostty from #58952, so this is app-independent - Claude Code running inside it; a git worktree under
~/Documents - Zero system extensions (
systemextensionsctl list→0 extension(s)), no DLP/EDR — refutes the DLP suspicion in #58952
Symptoms (matching #58952)
- Mid-session, with no prompt and no user action, every read of a pre-existing file under
~/Documentsreturns EPERM —ls,cat,git, and Claude Code's own Read tool, across the terminal's entire process tree including the app process itself stat/ls -lon a denied file still work; onlyopen()and directory listing fail- Fresh writes into
~/Documentssucceed and read back fine ~/Desktop,~/Downloads,/tmp, other apps: unaffected
Debugging trap: check whether log is shadowed
My first queries of log show --predicate 'subsystem == "com.apple.TCC"' returned nothing — same dead end as #58952's "no TCC log entries." The cause was a shell function shadowing log. Use /usr/bin/log explicitly. With the real binary, the denials are plainly visible — in the kernel Sandbox log, not TCC's:
$ /usr/bin/log show --last 5m --predicate 'sender == "Sandbox" AND eventMessage CONTAINS "System Policy"' --style compact | grep deny
kernel[0] (Sandbox) System Policy: cat(14096) deny(1) file-read-data /Users/me/Documents/<project>/AGENTS.md
kernel[0] (Sandbox) System Policy: Orca(1051) deny(1) file-read-data /Users/me/Documents/<project>/.git
Note the responsible app process itself (Orca(1051)) is denied — this is not a child-attribution problem.
Why "fresh writes succeed" (the fingerprint that seemed impossible)
#58952 flagged this as conflicting with a "kernel taints the tree" model. It doesn't: macOS stamps files at creation with a com.apple.macl extended attribute granting the creating app per-file access, and that grant is honored before the folder-level policy. Verified live:
$ echo probe > ~/Documents/.probe && xattr -l ~/Documents/.probe
com.apple.macl: 0000 00 C1 <uuid...> # per-file grant → readable
$ xattr -l ~/Documents/<pre-existing-file> # no macl for this app → folder policy applies → denied
So the fingerprint is exactly what a folder-level deny looks like, with per-file grants punching through.
Forensic timeline of the live incident (all times local, from /usr/bin/log show)
| Time | Event |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 18:09 | Terminal-hosted processes read ~/Documents files normally |
| 18:26–18:28 | Background XProtect scan sweep active (XProtect.daemon.scan, YARA load, syspolicyd) |
| 18:27:45 – 18:28:22 | Burst of TCC evaluations via sandboxd for helper processes with responsible={...identifier=com.stablyai.orca...}. tccd answers kTCCServiceSystemPolicyDocumentsFolder → authValue=2 (ALLOW) every time |
| 18:28:22 | tccd lazily creates a new record for the terminal under Tahoe's new service: Publishing <TCCDEvent: type=Create, service=kTCCServiceSystemPolicyAppData, identifier=com.stablyai.orca> to 3 subscribers |
| 18:28:34 → 18:43 | No further DocumentsFolder queries — idle window, no file access attempted |
| 18:43:10.577 | First read attempt: tccd asked only kTCCServiceSystemPolicyAllFiles (FDA) → deny (normal, app has no FDA) |
| 18:43:10.712 | 135 ms later the kernel denies the Documents read — the DocumentsFolder fallback query never happens |
| after | Every denial is served with zero tccd traffic — fresh processes included (verified: triggered a cat denial while capturing; tccd logged nothing) |
The healthy fallback is observable minutes earlier in the same log for another folder: AllFiles → deny, then kTCCServiceSystemPolicyDownloadsFolder → allow. For Documents, after the corruption, that second consult is skipped and the deny comes straight from kernel-cached state.
Root cause
The kernel caches System Policy verdicts keyed to the responsible app. The TCCDEvent published at 18:28:22 (lazy creation of the new-in-Tahoe kTCCServiceSystemPolicyAppData record, mid-burst, during an XProtect sweep) flushed or poisoned the cached Documents=allow verdict. Afterward the kernel denies without re-consulting tccd — whose database still says allow.
This explains all the folklore:
- No prompt, no TCC log at deny time — the deny never reaches tccd
- Re-granting in System Settings doesn't help — it edits a database that already says allow; the poisoned kernel copy is untouched
- Restarting the terminal fixes it — the cache entry is keyed to the responsible process and dies with it
- Non-deterministic recurrence — it needs the lazy AppData record creation (a once-per-app event... though repeated incidents suggest other events can trigger the same flush) racing with a verdict-evaluation burst
The in-place fix (verified live)
# find the bundle id of the terminal hosting the session
defaults read /Applications/<Terminal>.app/Contents/Info CFBundleIdentifier
tccutil reset SystemPolicyDocumentsFolder <that-bundle-id>
# touch any file under ~/Documents → permission prompt appears → Allow
Recovery was instant — no terminal restart, session fully preserved. The log shows why it works: the reset publishes TCCDEvent: type=Delete for the DocumentsFolder record, which flushes the poisoned kernel entry; the next access re-consults tccd (AUTHREQ_PROMPTING), prompts, and writes a fresh allow record (type=Create).
18:58:35.993 tccd: Publishing <TCCDEvent: type=Delete, service=kTCCServiceSystemPolicyDocumentsFolder, identifier=com.stablyai.orca>
18:58:36.048 tccd: AUTHREQ_PROMPTING: service=kTCCServiceSystemPolicyDocumentsFolder, subject=Sub:{com.stablyai.orca}...
18:58:40.141 tccd: Publishing <TCCDEvent: type=Create, service=kTCCServiceSystemPolicyDocumentsFolder, identifier=com.stablyai.orca>
How to reproduce the diagnosis on your machine (next incident)
- Confirm the fingerprint: fresh write into
~/Documentssucceeds, pre-existing read EPERMs. /usr/bin/log show --last 5m --predicate 'sender == "Sandbox" AND eventMessage CONTAINS "System Policy"' | grep deny— absolute path, in caselogis shadowed.- Find onset: histogram the denials per minute over the day with the same predicate and a wider
--start. - Around onset − 20 min, dump
--predicate 'process == "tccd"'and look forAUTHREQ_CTX/AUTHREQ_RESULTpairs (match onmsgID) forkTCCServiceSystemPolicyDocumentsFolder, plus anyTCCDEventpublications for your terminal's bundle id — I'd expect an event (AppDataCreateor otherwise) shortly before the last DocumentsFolder allow, then silence. - Apply the
tccutil resetfix; confirm theDelete/AUTHREQ_PROMPTING/Createsequence in the log.
If others can confirm the event-before-silence pattern (step 4), that pins the kernel cache-invalidation path as the bug for Apple — this needs a Feedback Assistant report with exactly this shape of log evidence.
Relationship to existing issues
- #58952: confirms symptoms; refutes its DLP theory (no system extensions here) and its "no TCC log entries" observation (denials are in the kernel Sandbox log; also beware
logshadowing); explains its H1/H2 refutations (FDA entries and csreq were never the problem — the DB was always right); resolves the "fresh-write conflict" with the macl xattr mechanism. - #49820 / #21942 / #21654: same fingerprint; the fix above should apply.