[BUG] Start-up hangs forever (main thread in open()) when a permission rule's path resolves to a TCC-protected location and the process lacks access
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code appears to read the target of each Read(...) permission rule during start-up. If a rule's path resolves into a macOS privacy-protected (TCC) location, such as a folder on a removable volume, and the launching process has no access to it, the CLI's main thread blocks in open() waiting for a consent decision that a background process can never get.
- The session never emits
system/initand never starts its first turn. It waits forever. - Nothing is logged: the debug log simply stops.
- Because the event loop is frozen, child processes it already spawned are never reaped. Ours was a
git cloneof the plugin marketplace, left as a zombie.
Reproduced on 2.1.289 (latest) and 2.1.281. 2.1.173 does not do this with the same configuration.
The minimal trigger is one rule, Read(//Users/<me>/backup/**), where ~/backup is a symlink to a folder on an external APFS drive. No MCP servers, plugins or hooks are needed. It was launched through the Agent SDK (@anthropic-ai/claude-agent-sdk 0.3.289, SDK mode) by a Node process under pm2: a background process without Full Disk Access, on macOS 15.4.1 (Apple silicon). The same query works from an SSH shell, because Remote Login has Full Disk Access.
What Should Happen?
Permission rules should not require reading the target's data at start-up. If resolution is needed, it should be one of:
- metadata-only (
lstat/readlink); - off the main thread, with a timeout, treating an unreadable target as "rule applies, target unreadable".
Either way, a rule pointing at an inaccessible location should never block the session, and failures should be logged.
Error Messages/Logs
Kernel log (log show --predicate 'eventMessage CONTAINS "System Policy: claude("'):
System Policy: claude(<pid>) deny(-1) file-read-data /Volumes/External/somedir
(becomes deny(4) once the hung process is killed)
tccd: a kTCCServiceSystemPolicyRemovableVolumes request attributed to the launching node process
sample <pid>: the main thread sits in openat$NOCANCEL -> __openat_nocancel for the whole sample; about 0.3 s of CPU in total
SDK message stream: with a string prompt, nothing at all; with streaming input, system/session_state_changed (running) and rate_limit_event (allowed), then nothing. Never system/init.
--debug-file: start-up proceeds normally (startupWorkGate opens), then stops right before the agents-md prompt.context hook and "[engine] turn 1 start"; nothing further is ever logged
Steps to Reproduce
- On macOS, create a symlink in your home folder to a folder on an external (removable) drive:
ln -s /Volumes/External/somedir ~/link - Save this as
repro.mjs(Agent SDK 0.3.289, which bundles Claude Code 2.1.289):
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const m of query({
prompt: "Reply with OK",
options: { disallowedTools: ["Read(//Users/<me>/link/**)"] },
})) {
console.log(m.type, m.subtype ?? "");
}
- Run it as a background process without Full Disk Access, for example
pm2 start repro.mjs --no-autorestartor a launchd agent. Don't run it from Terminal or an SSH session, which have broader access. - Nothing is printed, no
system/initis emitted, and the process never exits. The kernel log showsSystem Policy: claude(<pid>) deny(-1) file-read-data /Volumes/External/somedir. - Control: remove the
disallowedToolsrule, or run the same script from an SSH shell, and it completes in about 2 s.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.173
Claude Code Version
2.1.289 (Claude Code), also reproduced on 2.1.281
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
_No response_