[BUG] PreToolUse hooks silently do not fire for one working directory while permissions.deny from the same settings.json does

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 10, 2026

Preflight Checklist

  • [x] I have searched existing issues — see "Relationship to existing reports" below; this is deliberately narrower than the known -p hook 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

  1. 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.

  1. 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

View original on GitHub ↗