Preflight Checklist
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
Preflight Checklist
-phook cluster and the distinguishing evidence is statedWhat's Wrong?
For one working directory, a
PreToolUsehook registered in that directory's.claude/settings.jsonnever fires, whilepermissions.denyfrom 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
denyrule from the same settings file does fire in the same session, the file is demonstrably being read. Only thehooksblock 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):
{ "permissions": { "deny": ["Bash(*probe-token*)"] }, "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "node \"/abs/path/to/guard.js\"" } ] } ] } }guard.jsreads thePreToolUsepayload on stdin and, for a matching command, writes:{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"UNIQUE-MARKER-STRING …"}}Steps
~/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.
claude -p 'Run exactly this Bash command and nothing else: echo "DROP TABLE some_table"'→ Command runs.
UNIQUE-MARKER-STRINGnever appears. The hook produced no verdict.Controls already run, so these can be skipped in triage
PreToolUsepayload on stdin it returns the correct JSONpermissions.denyfrom that same file fires (step 1)node "<script>"form and a wrapper-command formhasTrustDialogAccepted: trueset for that directoryWhat Should Happen?
A
PreToolUsehook registered in a directory's.claude/settings.jsonshould fire wheneverpermissionsfrom that same file are in force. Either both apply or neither does; a settings file that is loaded enough to enforcedenybut not enough to runhooksis 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
PreToolUsehooks do not fire in headless-pmode: #30143, #33343, #34240, #35557, #36071, #40506 (and #34692 for subagents). All are closed asnot_plannedor 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,
PreToolUsehooks did fire underclaude -pin 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