PreToolUse:Bash hooks fail with MODULE_NOT_FOUND (fail-open) in auto permission mode when project is on an external volume (macOS, v2.1.220)
Summary
After switching permission mode to "auto", every Bash tool call made by subagents spawned under that mode shows a PreToolUse:Bash hook error pair:
PreToolUse:Bash hook error
Failed with non-blocking status code: node:internal/modules/cjs/loader:1408
The hook's node process fails with MODULE_NOT_FOUND for the hook script itself — the script never executes. Because the failure is non-blocking, the tool call proceeds, so a policy-enforcement hook silently fails open (see Security impact below).
Environment
- Claude Code v2.1.220 (native build, Mach-O arm64), macOS Darwin 25.5.0
noderesolved by hooks: v23.11.0- Project directory lives on an external APFS volume and the path contains a space:
/Volumes/Nvme3 1TB/Documents/<project> - Hook registered in project
.claude/settings.local.json:
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-git-push-authority.cjs\"",
"timeout": 10
}
]
}
]
The hook file is dependency-free (only require('fs')), so the MODULE_NOT_FOUND is for the entry file itself, not an inner require.
Observed behavior / evidence
- Exact temporal correlation with the mode switch. From the session transcript (timestamps UTC): a background subagent was spawned at
12:41:48Z, thepermission-modeentry flips to"auto"at12:41:54Z, and from that point on every Bash call from agents spawned under auto mode produced the error pair. Commands themselves still ran fine. - Agents spawned before the mode switch are unaffected. A background subagent launched ~3h earlier (default mode) kept running in the same session; I temporarily instrumented the hook to log
process.env.CLAUDE_PROJECT_DIR— for that older agent's Bash calls the hook executed normally with the correct value, at the same time the newer agents were failing. - Only
Bash-matcher hooks fail. AWrite|Edit-matcher PreToolUse hook in the same.claude/hooks/directory, registered the same way with$CLAUDE_PROJECT_DIR, never errored — consistent with the failure being tied to the sandboxed Bash execution path rather than to hooks in general. - The error signature is reproducible in two ways (byte-identical stderr, exit 1):
- Sandbox denying reads on the external volume:
``bash`
sandbox-exec -p '(version 1) (allow default) (deny file-read* (subpath "/Volumes"))' \
node "/Volumes/Nvme3 1TB/Documents/<project>/.claude/hooks/enforce-git-push-authority.cjs"
MODULE_NOT_FOUND
(Node's CJS resolver reports access-denied stats as → loader:1408`.)
CLAUDE_PROJECT_DIRempty:node "/.claude/hooks/enforce-git-push-authority.cjs".
I could not determine from the compiled binary which of the two mechanisms applies (hook process spawned inside the Bash sandbox profile that can't read the external volume, vs. CLAUDE_PROJECT_DIR not injected on that code path), but the trigger is clearly the auto permission mode.
Expected behavior
Hooks should run with the same environment and (un)sandboxing regardless of permission mode. The build's own internal text describes background monitors as "unsandboxed, same trust tier as hooks", which suggests hooks are intended to run unsandboxed.
Security impact
The failing hook is a permission gate (it emits permissionDecision: "deny" for git push / gh pr create / gh pr merge unless a specific agent role is active). Since PreToolUse hook failures are non-blocking, the gate is silently not enforced for every affected Bash call — and this happens precisely in auto mode, where commands are being auto-approved. A hook infrastructure failure that disables a deny-gate arguably should fail closed, or at least be surfaced more loudly than a per-call console line.
Steps to reproduce
- Put a project on an external volume (path with a space, e.g.
/Volumes/My SSD/proj). - Register a PreToolUse hook with
matcher: "Bash"as above (node "$CLAUDE_PROJECT_DIR/.claude/hooks/whatever.cjs"). - Start Claude Code, switch permission mode to auto.
- Spawn a subagent that runs any Bash command (background
Task/Agentreproduces it reliably). - Every Bash call shows
PreToolUse:Bash hook error / Failed with non-blocking status code: node:internal/modules/cjs/loader:1408, while the same hook works for sessions/agents in default mode.