[BUG] PreToolUse hooks silently do not fire for one working directory while permissions.deny from the same settings.json does
Preflight Checklist
- [x] I have searched existing issues — see "Relationship to existing reports" below; this is deliberately narrower than the known
-phook cluster and the distinguishing evidence is stated - [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
For one working directory, a PreToolUse hook registered in that directory's .claude/settings.json never fires, while permissions.deny from the same file fires reliably. The hook produces no output, no error, and no log entry — the command simply runs.
The control is what makes this worth reporting: because a deny rule from the same settings file does fire in the same session, the file is demonstrably being read. Only the hooks block is inert.
I could not isolate the responsible variable, so this reports the reproducible observation and the controls rather than a proposed cause.
Setup (paths generalized):
~/some-dir/ <- working directory, NOT a git repository
.claude/settings.json
{
"permissions": {
"deny": ["Bash(*probe-token*)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"/abs/path/to/guard.js\"" }
]
}
]
}
}
guard.js reads the PreToolUse payload on stdin and, for a matching command, writes:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"UNIQUE-MARKER-STRING …"}}
Steps
- From
~/some-dir, run a command matching the deny rule:
claude -p 'Run exactly this Bash command and nothing else: echo probe-token'
→ Correctly denied. This proves the settings file is loaded.
- From the same directory, run a command the hook is written to intercept:
claude -p 'Run exactly this Bash command and nothing else: echo "DROP TABLE some_table"'
→ Command runs. UNIQUE-MARKER-STRING never appears. The hook produced no verdict.
Controls already run, so these can be skipped in triage
| Check | Result |
|---|---|
| Is the hook script reachable/valid? | Yes — invoked directly with a real PreToolUse payload on stdin it returns the correct JSON |
| Is the settings file loaded? | Yes — permissions.deny from that same file fires (step 1) |
| Is it the hook command form? | No — reproduced with both an absolute-path node "<script>" form and a wrapper-command form |
| Is it workspace trust? | No — reproduced with hasTrustDialogAccepted: true set for that directory |
| Is the hook script outside the project dir? | No — reproduced with the script both inside and outside it |
What Should Happen?
A PreToolUse hook registered in a directory's .claude/settings.json should fire whenever permissions from that same file are in force. Either both apply or neither does; a settings file that is loaded enough to enforce deny but not enough to run hooks is a silent partial failure, which is the dangerous shape — a hook-based guard reads as installed and enforcing when it is not.
Relationship to existing reports
There is an existing cluster reporting that PreToolUse hooks do not fire in headless -p mode: #30143, #33343, #34240, #35557, #36071, #40506 (and #34692 for subagents). All are closed as not_planned or stale, most are locked, so this is filed as a new issue rather than a comment.
This report is deliberately narrower, and one observation differs from that cluster's stated symptom — noted for completeness, not as a rebuttal. In my testing on the current version, PreToolUse hooks did fire under claude -p in several other working directories, including one that is not a git repository. So the behaviour I hit is not "hooks never fire in -p" as those issues describe; it is directory-specific, with the deny-rule control passing in the same session. Boundary stated plainly: I could not determine what distinguishes the directories where it fires from the one where it does not — a non-git directory reproduced the failure, but another non-git directory did not, so "non-git" is a correlate rather than a demonstrated cause.
If maintainers consider this within the closed cluster's scope, closing it as such is a reasonable outcome — the useful part is the deny-fires-while-hooks-do-not control, which I did not find in those reports.
Environment
- Claude Code: current version (both the npm CLI and the VS Code bundled binary reproduce the deny-fires-hooks-silent asymmetry for the affected directory)
- OS: Windows 11 host with WSL2 (Ubuntu); the affected directory is on the WSL filesystem, reached from the Windows side over a UNC path
- Node: v24