macOS: CLI installed as bare Mach-O at a versioned path gets a new TCC identity on every auto-update (path-keyed, client_type=1)

Status Open
Reported on v2.1.233
Maintainer reply None cached
Activity 1 comment · opened Aug 16, 2026

Summary

On macOS, Claude Code CLI is installed as a bare Mach-O executable at a version-specific path that the auto-updater changes on every release. Under launchd (no responsible GUI app), macOS TCC indexes the consent by absolute executable path (client_type=1) rather than by the signing identifier com.anthropic.claude-code.

Because the path changes with every update, each release creates a fresh, unconsented TCC identity, and the user is re-prompted for Full Disk Access, Desktop, Documents, Downloads and Photos. On a machine that updates roughly daily this is a burst of ~5 modal dialogs per day.

The same product, when packaged as an app bundle, does not have this problem — see the contrast below. This looks like a packaging issue, not a macOS misconfiguration.

Environment

| | |
|---|---|
| macOS | 26.6.1 (25G76), arm64 |
| Claude Code | 2.1.233, native installer, ~/.local/share/claude/versions/2.1.233 |
| Signature | Developer ID Application: Anthropic PBC (Q6L2SF6YDW), valid, notarized |
| Affected range | first observed at 2.1.214 (2026-07-18); last bundle-keyed grant 2026-07-17 |

Symptom

Repeated TCC prompts, 2–100/day depending on activity. For the bundled variant the dialog is verbatim:

"claude.app" would like to access data from other apps.
Keeping app data separate makes it easier to manage your privacy and security.
                                            [Don't Allow]  [Allow]

For the CLI variant the prompts are Full Disk Access and the per-folder ones (Desktop / Documents / Downloads / Photos). (Their literal strings were not captured — macOS does not persist tccd Info-level entries, so they could not be recovered after the fact. The database evidence below is what documents them.)

The packaging fact

$ codesign -dvvv ~/.local/share/claude/versions/2.1.233
Identifier=com.anthropic.claude-code
Format=Mach-O thin (arm64)          # not "app bundle with Mach-O"
Authority=Developer ID Application: Anthropic PBC (Q6L2SF6YDW)
Info.plist=not bound                # no Info.plist in the signature
Sealed Resources=none               # no bundle

$ spctl -a -vvv -t exec ~/.local/share/claude/versions/2.1.233
rejected (the code is valid but does not seem to be an app)

$ readlink ~/.local/bin/claude
/Users/<user>/.local/share/claude/versions/2.1.233   # changes on every release

The signature is valid. The issue is that there is no bundle, and the install path is version-specific.

The contrast: same product, two packagings, two persistence behaviours

Both are signed by Anthropic with identifier com.anthropic.claude-code. Only the packaging differs.

| | Claude Code embedded in Claude Desktop | Claude Code CLI |
|---|---|---|
| Path | ~/Library/Application Support/Claude/claude-code/2.1.229/claude.app | ~/.local/share/claude/versions/2.1.233 |
| Packaging | app bundle (Info.plist, CodeResources, sealed) | bare Mach-O (Info.plist=not bound) |
| TCC client_type | 0 — keyed by bundle ID | 1 — keyed by absolute path |
| Survives an update? | yes (bundle ID is stable) | no (path changes every release) |

This is the core of the report: the bundled variant persists across updates, the bare-binary variant does not.

Evidence

1. Orphaned TCC identities — user database

-- ~/Library/Application Support/com.apple.TCC/TCC.db
SELECT client, client_type, COUNT(*), MIN(datetime(last_modified,'unixepoch'))
FROM access WHERE client LIKE '%claude%' GROUP BY client, client_type;

14 distinct versioned paths, 65 rows, all client_type=1:

…/versions/2.1.214 | 1 | 2 rows | 2026-07-18 17:15:23
…/versions/2.1.215 | 1 | 5      | 2026-07-19 11:45:05
…/versions/2.1.219 | 1 | 4      | 2026-07-25 10:22:42
…/versions/2.1.220 | 1 | 5      | 2026-07-28 07:10:08
…/versions/2.1.221 | 1 | 5      | 2026-08-04 06:46:46
…/versions/2.1.222 | 1 | 6      | 2026-08-05 06:56:26
…/versions/2.1.223 | 1 | 4      | 2026-08-06 21:47:29
…/versions/2.1.224 | 1 | 5      | 2026-08-07 09:47:01
…/versions/2.1.225 | 1 | 5      | 2026-08-08 20:48:18
…/versions/2.1.226 | 1 | 5      | 2026-08-09 18:08:31
…/versions/2.1.227 | 1 | 6      | 2026-08-11 13:41:55
…/versions/2.1.231 | 1 | 4      | 2026-08-13 22:19:45
…/versions/2.1.232 | 1 | 4      | 2026-08-14 12:01:47
…/versions/2.1.233 | 1 | 5      | 2026-08-15 12:02:24

Services re-prompted per version: SystemPolicyDesktopFolder, SystemPolicyDocumentsFolder, SystemPolicyDownloadsFolder, SystemPolicyNetworkVolumes, MediaLibrary, sometimes SystemPolicyAppData.

For comparison, while the client was still keyed by bundle ID, 6 grants covered 3 months (2026-04-09 → 2026-07-17) and never re-prompted.

2. Full Disk Access denials — system database

-- /Library/Application Support/com.apple.TCC/TCC.db
SELECT client, client_type, auth_value, datetime(last_modified,'unixepoch')
FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND client LIKE '%claude%';

14 rows, one per version, every one auth_value=0 (denied):

com.anthropic.claude-code    | 0 | 0 | 2026-07-03 18:00:54   <- bundle-keyed, pre-regression
…/versions/2.1.214           | 1 | 0 | 2026-07-18 17:15:10
…/versions/2.1.215           | 1 | 0 | 2026-07-19 05:15:22
…/versions/2.1.219           | 1 | 0 | 2026-07-25 00:58:43
…/versions/2.1.220           | 1 | 0 | 2026-07-28 00:59:46
…/versions/2.1.221           | 1 | 0 | 2026-08-04 01:10:29
…/versions/2.1.222           | 1 | 0 | 2026-08-05 01:11:21
…/versions/2.1.223           | 1 | 0 | 2026-08-06 07:12:24
…/versions/2.1.224           | 1 | 0 | 2026-08-07 09:27:18
…/versions/2.1.225           | 1 | 0 | 2026-08-08 03:27:56
…/versions/2.1.226           | 1 | 0 | 2026-08-09 17:53:01
…/versions/2.1.227           | 1 | 0 | 2026-08-10 23:54:40
…/versions/2.1.231           | 1 | 0 | 2026-08-13 18:00:05
…/versions/2.1.232           | 1 | 0 | 2026-08-14 12:01:22
…/versions/2.1.233           | 1 | 0 | 2026-08-15 00:01:49

The two databases agree: the FDA denial for 2.1.214 (17:15:10) precedes the first folder row (17:15:23) by 13 seconds. Note nine of the fourteen are between 00:00 and 05:59 — unattended scheduled runs.

3. Why this is invisible in interactive testing (the A/B control)

This is the part worth reading if you cannot reproduce it. The same binary, on the same machine, 17 minutes apart, produces different TCC attribution depending on launch context.

Run from Terminal.app (which has Full Disk Access):

AttributionChain: responsible={identifier=com.apple.Terminal, …},
                  accessing={identifier=com.anthropic.claude-code,
                             binary_path=…/claude-probe}
Handling access request to kTCCServiceSystemPolicyAllFiles,
  from Sub:{com.apple.Terminal} … ReqResult(Auth Right: Allowed (System Set))

→ The subject is Terminal, Terminal's FDA satisfies the check, no folder service is ever consulted, no prompt.

Run from launchd (no responsible GUI app):

IDENTITY_ATTRIBUTION: /Users/<user>/.local/share/claude/versions/2.1.233 …, type 1
AttributionChain: responsible={identifier=com.anthropic.claude-code,
                               responsible_path=…/versions/2.1.233},
                  accessing={identifier=com.anthropic.claude-code,
                             binary_path=…/versions/2.1.233}
Handling access request to kTCCServiceSystemPolicyDesktopFolder,
  from Sub:{/Users/<user>/.local/share/claude/versions/2.1.233}
  … ReqResult(Auth Right: Unknown (None), promptType: 1)

→ The subject is the path, identity type 1, and Auth Right: Unknown (None) on a new version is exactly the prompt condition.

Consequence for reproduction: if you test interactively from a terminal that has Full Disk Access, you will never see this. It only manifests when the CLI runs without a responsible GUI app.

To reproduce, run the CLI from a LaunchAgent (any trivial claude -p '…') across a version bump and inspect both TCC databases with the queries above.

Open question about the mechanism — explicitly NOT a claim

We can only infer behaviour from the outside; we cannot read TCC's indexing rules.

Observation that needs explaining: in the same log line set, tccd does resolve identifier=com.anthropic.claude-code for the process (so the signing identity is available to it), yet the access request is recorded with Sub:{<absolute path>} and stored as client_type=1.

Unproven hypothesis: with no responsible app to attribute the request to, TCC falls back to the executable itself; if that executable is a bundle it has a stable handle (CFBundleIdentifierclient_type=0), and if it is a bare Mach-O it has none, so the absolute path is stored as the handle (client_type=1) even though the signing identifier is still read for logging.

We are flagging this as unproven on purpose. Two other mechanism explanations were investigated and falsified during this diagnosis:

  1. "The Contents/Helpers/disclaimer wrapper used to launch MCP servers causes the re-prompt" (cf. #36832) — falsified here: the affected machine has 0 MCP servers configured in Claude Desktop, so the prompt count cannot correspond to MCP servers.
  2. "TCC cannot key on the bundle ID because Info.plist is not bound, so it falls back to path"falsified: TCC resolved com.anthropic.claude-code for the very same binary hardlinked to a brand-new, never-seen path.

Anthropic can answer this by reading the installer and testing both packagings; we cannot.

Impact

  • Not machine-specific. Anyone who invokes the Claude Code CLI from launchd (or any context without a responsible GUI app — scheduled agents, CI runners on macOS, cron, background daemons) will accumulate a new orphaned TCC identity on every release.
  • Grows monotonically. On the reporting machine, 77 of the 235 rows in the user TCC database — about a third — are Claude Code version churn accumulated in one month.
  • Trains users to click through security dialogs, and pushes them toward granting Full Disk Access to a self-updating, user-writable binary that also ships disable-library-validation and allow-unsigned-executable-memory — a materially worse security posture than the prompts themselves.
  • The only user-side workaround is DISABLE_AUTOUPDATER=1, i.e. giving up automatic security patches for the CLI in exchange for stable permissions. That is a bad trade to have to make.

Suggested directions (for triage, not prescriptive)

  1. Ship the CLI inside a signed .app bundle — the embedded Claude Desktop variant already is one and does not exhibit the problem.
  2. Or install to a stable path (e.g. ~/.local/share/claude/current/claude) with the version churn kept behind it, so the executable path TCC sees does not change per release.
  3. Whichever is chosen, a one-time migration note would help users clear the accumulated orphaned entries.

Attachments available on request

Raw tccd live captures for both launch contexts, both TCC database dumps, and the full local diagnosis.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗