TL;DR: Claude Code auto-updates into a new versioned path every day or two. macOS TCC grants are keyed to the resolved binary path, so every update resets your grants to zero. In headless launchd daemons (no GUI session, no human to click "Allow"), that reset can cause a silent, infinite hang. Fix it: DISABLE_AUTOUPDATER: "1" in ~/.claude/settings.json + one Full Disk Access grant. Then add a preflight canary so you know immediately if something breaks again.

The Setup

I've been running a headless claude -p daemon I use for my morning briefing — Claude's non-interactive, scriptable mode — under launchd (macOS's background scheduler) that fires at 8 AM, pulls my task list from Things 3, digests my email, and delivers a morning summary.

When it works, it's GREAT. When it doesn't…

The Wall

One morning it just didn't show up.

I started chasing theories. First I blamed a new Claude build. Then I looked at the TCC grant timestamps in the macOS permission database and saw something alarming: grantless binary at 08:00, first grant logged at 08:27, MediaLibrary at 08:32, AppleEvents at 08:41.

So the daemon launched grantless and apparently just… waited.

I declared TCC the root cause. I was wrong.

What I Actually Got Wrong

The real killer that morning was email overflow — 146 unread messages, a big marketing blast, over one million characters of raw email body piped through the MCP tool. The model choked on it.

I know this because when I ran a streamed test with all TCC grants present and Full Disk Access already on, the daemon still hung. That's your proof. TCC wasn't holding the gun.

But chasing the wrong suspect for an hour forced me to actually understand what TCC was doing.

What I Found About TCC

macOS TCC (Transparency, Consent, and Control — the system that gates access to contacts, calendar, disk, AppleEvents, etc.) grants permissions keyed to the resolved binary path.

Claude Code's native installer drops each version at:

~/.local/share/claude/versions/<version>/claude

So version 2.1.178 and 2.1.179 are literally two different strangers to TCC. The symlink at ~/.local/bin/claude updates, the grants from yesterday don't follow.

You can verify this yourself:

sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT service,client,auth_value FROM access"

You'll see one row per version path. auth_value=2 means allowed — but only for that path.

And here's the part that's genuinely scary if you're running headless daemons: interactive sessions just show a popup you click. Headless launchd processes have no GUI session to answer the prompt. The gated call blocks. The watchdog eventually kills it. You get silence.

This is exactly what broke my /mem-search daemon a couple months earlier — same class of bug, different project, same frustrating mystery.

The Fix That Actually Worked

Freeze the path. Add this to ~/.claude/settings.json:

{ "env": { "DISABLE_AUTOUPDATER": "1" } }

Grant once. It sticks — until you deliberately run claude update, at which point the path changes again and you re-grant once.

Grant Full Disk Access once in System Settings → Privacy & Security → Full Disk Access. This covers the SystemPolicyAppData prompts ("…access data from other apps") in a single persistent grant, including reading the Things 3 database.

Dead ends I already tried so you don't have to:

  • tccutil only resets — can't grant
  • TCC.db is SIP-protected, so you can't script an "allow"
  • npm install gives you the same versioned-path problem
  • PPPC profiles require an approved MDM — not happening on a personal Mac

Add a preflight canary. At the top of any headless claude -p daemon, run a cheap ~20-second test call. If it times out, alert loud and exit fast — don't let a grantless binary block until launchd's watchdog mercy-kills it twenty minutes later.

Why This Actually Matters

The TCC issue wasn't what took down my morning briefing daemon that morning. But it was a real landmine sitting in my stack, silently waiting for the next overnight update to orphan my grants again.

If you're running any headless claude -p process under launchd or cron — touching contacts, calendar, AppleEvents, other apps' data — this will eventually bite you. Probably at the worst possible time, probably silently.

Freeze the path. Grant once. Canary the run. That's the three-step pattern I now bake into every daemon I ship.

P.S. The popup showing a bare version string like "2.1.178" instead of "Claude Code"? That's because the versioned binary has no CFBundleName. You're not imagining it — macOS genuinely doesn't know what app that is.