Post/PreToolUse Hooks Not Executing in Claude Code
Bug Report for Anthropic - Claude Code Hook System
Issue: PreToolUse and PostToolUse Hooks Not Executing
Environment:
- Claude Code Version: claude-sonnet-4-20250514
- Platform: macOS Darwin 24.6.0
- Configuration: .claude/settings.local.json
Problem:
PreToolUse and PostToolUse hooks are configured correctly but never execute, while other hook types work perfectly.
Evidence:
✅ WORKING HOOKS:
- Stop hooks execute consistently (proven by debug logs showing 50+ executions)
- SubagentStop hooks work (sound notifications trigger)
- UserPromptSubmit hooks work (manual /aw command functions)
❌ BROKEN HOOKS:
- PreToolUse hooks never execute despite proper configuration
- PostToolUse hooks never execute despite proper configuration
Configuration Tested:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'PreToolUse triggered' >> /tmp/pretool-test.log"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cat /Users/greg/codebase/stayinpattaya/.claude/commands/aw.md"
}
]
}
]
}
}
Testing Performed:
- Verified hook scripts execute correctly when run manually
- Tested multiple matchers: "*", "Bash", "Task", "Write", "Edit"
- Tested both simple commands and complex scripts
- Confirmed proper JSON input handling with jq parsing
- Verified file permissions and absolute paths
- Cleared logs and performed fresh tests
- Compared working Stop hook configuration (identical format)
Expected Behavior:
PreToolUse and PostToolUse hooks should execute before/after tool usage and show output or create logs.
Actual Behavior:
Tool-related hooks never execute. No output displayed, no log files created, no evidence of execution.
Conclusion:
PreToolUse and PostToolUse hook events are not being fired by Claude Code, despite proper configuration following official documentation. Other hook types (Stop, SubagentStop,
UserPromptSubmit) function correctly using identical configuration patterns.
This appears to be a selective hook system failure affecting only tool-related events.
36 Comments
Claude Code Hooks: A Guide to the Session Security Feature
Overview
Claude Code includes a crucial security feature that governs how and when changes to hooks are applied. This feature prevents hooks from being modified or injected during an active session, protecting you from unintended or malicious code execution.
This behavior is a deliberate security design, not a bug. This guide explains how it works and the correct methods for managing your hooks.
The Security Feature Explained
How It Works
~/.claude/settings.json,.claude/settings.json, etc.).Why This Exists
This security model is designed to protect against:
Common Scenarios and Symptoms
You may have encountered this feature if:
settings.jsonand saw a warning message in Claude Code about external modifications.These are all signs that the security feature is working as intended. The original hook snapshot taken at the start of your session is still in effect.
Applying Hook Changes: The Correct Methods
Because changes are not applied automatically, you must choose one of these two methods to activate them.
Method 1: The Recommended In-Session Method (
/hooksmenu)This is the best practice for updating hooks without disrupting your workflow, as it preserves your current conversation context.
settings.jsonfile and save it./hookscommand in your active Claude Code session.Use this method when: You are in the middle of a task and want to apply a hook change without losing your conversation history and context.
Method 2: Restarting the Session
This is a simple and foolproof method that ensures you start with a clean slate and the latest configuration.
settings.jsonfile.Ctrl+Dor theexitcommand).claude.The new session will take a fresh snapshot of your settings files, and your updated hooks will be active immediately.
Use this method when: You are not in the middle of a complex conversation, or you are setting up your environment before starting a new task.
Testing Hooks Properly
To avoid frustration, always configure hooks before you start the session you intend to test them in.
Correct Testing Workflow
Troubleshooting and Important Considerations
Troubleshooting Checklist
If your hooks aren't working as expected:
/hooksmenu or by restarting your session?settings.jsonfile valid JSON? (Verify withjq . .claude/settings.json)chmod +x your-script.sh)1.0.38or newer? (Hooks were released in this version).Settings Precedence
Hooks are loaded from multiple locations. It's crucial to understand which file takes precedence if hooks are defined in more than one place. The order is:
/etc/claude-code/managed-settings.jsonon Linux).claude/settings.jsonand.claude/settings.local.json)~/.claude/settings.json)Settings from a higher-precedence file will override those from a lower-precedence file.
What DOESN'T Apply Hook Changes
tmuxwindow or pane.source ~/.bashrcor similar shell reload commands.These actions typically do not terminate the underlying Claude Code process, so the original hook snapshot remains in memory.
Best Practices
.claude/settings.jsonbefore starting a major work session.READMEto explain what each hook does.Summary
The New Golden Rule: Hook changes must be explicitly applied. Use the
/hooksmenu to update your active session, or restart Claude Code to load a fresh configuration.This session-locking mechanism is a fundamental security feature that ensures the integrity of your development environment. By understanding how to work with it, you can customize Claude Code powerfuly and securely.
Can you retest with this in mind?
Because its working for me.
Your configuration is also working for me.
Yes, as a security feature, changes to Settings JSON files don't take effect immediately. You either have to acknowledge with
/hooksor start a new session.coygeek
i used the /hooks command and restarted the session over 50 times and it DOES NOT WORK
Reading 1: Hook changes must be explicitly applied using /hooks menu or restart
Reading 2: /hooks menu shows pending changes as diff and allows confirmation
Reading 3: Once confirmed, new hook configuration applies to current session
Reading 4: Security feature prevents automatic application during session
Reading 5: Method 1 (/hooks) preserves conversation context and should work
Reading 6: Troubleshooting checklist - did you apply changes? valid JSON? execute permissions? version 1.0.38+?
Reading 7: Settings precedence - project-level overrides user-level
Reading 8: What doesn't apply changes - tmux, terminal tabs, shell reloads
Reading 9: Configure first, test in isolation, document your hooks
Reading 10: The /hooks menu method SHOULD work for current session
The document says this SHOULD work and apply to the current session. But the PostToolUse hook still isn't firing.
its a very SIMPLE Too that is made over complicated that does not work easy.
There are 7 coders here with me on their own claude code accounts with all diffrent experance, they all say the same thing it's a simple too that shuld be simple to set up when this all started it delted all our json files we had settings in and now it's taking snapshots on start etc
Talk about making something so complicated to the point it DOES NOT WORK and stressed everyone out trying to use it when we followed the https://docs.anthropic.com/en/docs/claude-code/hooks
I can confirm this issue persists. I've extensively tested hooks following the documentation exactly:
Environment
Testing Performed
Evidence
echo '{"tool_input": {"file_path": "/tmp/test.md"}}' | ./hook.sh✅ WORKSKey Finding
The hook infrastructure exists and hooks execute correctly when called manually, but the trigger mechanism that should call hooks when tools are used is completely broken. This is not a configuration issue - the hooks simply never get invoked by Claude Code.
This makes the entire hooks feature unusable for its primary purpose of automating tasks like code formatting, linting, and validation.
Please prioritize fixing this as it's a critical feature that many users rely on.
yes pretooluse hook not working for me too. tried adding sound whenever it asks user for decision, but it never triggers.
Not working for me either. I was trying to follow another user's advice of adding an Always Works hook before Edits are made, but this does not seem to be triggering.
I have same issue, and I fixed it.
In September 2025, claude-code strengthened validation rules for settings.json. Any invalid configuration will disable hooks.
maybe there is something wrong with your claude setting.json , checkout the solutions I
https://github.com/dazuiba/CCNotify?tab=readme-ov-file#why-not-working
@Xopher00 @tytung2020 @Dmdv @fwends
Enterprise Confirmation: PreToolUse/PostToolUse Hooks Critical Failure
🏢 Enterprise Context
Team: Enterprise monorepo development team (500+ files)
Platform: macOS Darwin 24.6.0
Claude Code Version: Latest (as of 2025-09-11)
Impact: Critical security gap - preventive controls completely bypassed
✅ Confirming Identical Issue
We can confirm this exact same issue affects our enterprise environment. Like the other teams here, we have:
🚨 Security Impact
Our PreToolUse hooks are designed to prevent dangerous bulk operations that could corrupt our enterprise codebase. The failure of these hooks creates a critical security vulnerability:
Expected behavior: Block dangerous tools before they execute
Actual behavior: Hook never runs, dangerous operations proceed unchecked
Manual test:
python prevent_violations.py --tool-name="MultiEdit"✅ works perfectlyClaude Code trigger: ❌ Never executes
🔬 Additional Technical Evidence
Configuration Validation
jqchmod +xon all scripts)$CLAUDE_PROJECT_DIRset correctly)Hook Execution Pattern Analysis
| Hook Type | Reliability | Our Use Case | Status |
|-----------|-------------|--------------|---------|
| SessionStart | 100% | Environment setup | ✅ Working |
| PostToolUse | 100% | Dependency sync | ✅ Working |
| PreToolUse | 0% | Security prevention | ❌ BROKEN |
Testing Methodology
💼 Enterprise Requirements
As an enterprise team, we cannot use Claude Code in production until this is fixed because:
🎯 Specific Request to Anthropic
High Priority Fix Request: This issue is blocking multiple enterprise teams from using Claude Code's automation features safely.
Key debugging areas (based on our analysis):
$TOOL_NAMEand$TOOL_ARGSproperly set in PreToolUse context?🔄 Workaround Status
Currently using reactive PostToolUse validation instead of preventive PreToolUse blocking, but this is:
📞 Community Support
Happy to provide additional debugging information, test configurations, or assist with reproduction if needed by the Anthropic team.
This is affecting multiple enterprise teams - a fix would unblock significant Claude Code adoption in enterprise environments.
---
Environment Summary:
@matssun refer to https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-input
Your usage of $TOOL_NAME and $TOOL_ARGS is incorrect.
@matssun
Hey, thanks for posting such a detailed write-up.
It turns out that
PreToolUsehooks don't receive tool information via environment variables like$TOOL_NAMEor$TOOL_ARGS. Instead, Claude Code pipes a JSON object containing all the context to the standard input (stdin) of your hook command.The Root Cause
Based on the official Hooks Reference documentation, the
stdinfor aPreToolUsehook looks like this:Your script is expecting environment variables that are never set, which is why it seems like the hook never runs. The command executes, but the variables are empty, so your Python script likely exits without doing anything.
How to Fix It
You need to modify your hook's command to read from
stdin, parse the JSON, and then pass the values to your Python script. The easiest way to do this without changing your Python script is withjq.Here is the corrected version of your hook command:
Explanation:
cat | jq -c . | { ... }: This reads the entire JSON object fromstdin.read JSON_INPUT: This stores the JSON in a shell variable.TOOL_NAME_FROM_JSON=$(...): We usejqto extract the.tool_namestring.TOOL_ARGS_FROM_JSON=$(...): We usejqto extract the entire.tool_inputobject as a compact JSON string. Your Python script will need to be able to parse this JSON string from the--tool-argsargument.Recommended: Modify the Python Script
An even cleaner solution is to modify your
prevent_violations.pyscript to read directly fromstdin, which is the intended pattern for hooks. The Hooks Guide and Reference pages have great examples of this.Your Python script would start like this:
Then your
settings.jsonhook command becomes much simpler:Hope this helps you get unblocked!
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
I just tested this issue on Claude Code v2.0.61 (macOS Darwin 25.1.0) and can confirm that PreToolUse and PostToolUse hooks are now working correctly.
Test Setup
Results
| Tool | PreToolUse | PostToolUse |
|------|------------|-------------|
| Read | ✅ Working | ✅ Working |
| Write | ✅ Working | ✅ Working |
| Bash | ✅ Working | ✅ Working |
| Edit | ✅ Working | ✅ Working |
All hooks fired correctly and logged the expected tool names to the log files.
Notes for Anyone Still Experiencing Issues
$TOOL_NAME). Usecat | jq -r '.tool_name'to extract the tool name./hooksto apply changes or restart Claude Code.claude --version).This issue appears to be resolved as of v2.0.61.
This issue still persists and is a major security loophole.
### Environment
matcher: "*"in~/.claude/settings.json### Configuration
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "/Users/<user>/.local/bin/securityCheck",
"args": ["--hook-check"],
"timeout": 30
}]
}]
}
}
````
Observed Behavior
✅ Manual Test (Hook Binary Works)
echo '{"session_id":"test","tool_name":"Read","tool_input":{"file_path":"~/.agentfence/secrets/agentfence-secret.txt"}}' \
| /Users/<username>/.local/bin/agentguard --hook-check
Output: {"permissionDecision":"deny",...}
Hook correctly blocks access
⚠️ Hooks ARE Loading (Partial Evidence)
Critical finding: Hooks ARE being invoked for Bash commands, but NOT for Read/Write/Edit tools.
When running Bash commands in the session, Claude Code output shows:
⏺ Bash(echo "test")
⎿ PreToolUse:Bash hook error
This "PreToolUse:Bash hook error" message appears consistently for Bash tool usage, proving:
This suggests selective tool coverage, not a configuration issue.
❌ Actual Usage (Hook NOT Invoked for Read/Write Tools)
Test 1: Asked Claude to read protected file
Result: File read successfully, contents displayed
Evidence: No hook log entry at timestamp of Read operation
Test 2: Explicitly requested native Read tool
Result: File read successfully, secrets exposed
Evidence: No hook invocation logged
Test 3: Used MCP tool (alternative)
Result: Correctly blocked via MCP server
Evidence: MCP goes through daemon, native tools bypass completely
| Action | Tool Used | Hook Invoked? | Result |
|---------------|---------------|------------------|-------------------------|
| Manual test | CLI | ✅ Yes | BLOCKED correctly |
| Auto read | MCP read_file | ✅ Yes (via MCP) | BLOCKED correctly |
| Explicit read | Native Read | ❌ NO | BYPASSED - secrets read |
Security Implications
Root Cause Analysis
Two-layer failure:
Workaround
Using MCP layer instead:
Ask
For Anthropic Team:
For Community:
Has anyone successfully gotten PreToolUse hooks working for Read/Write/Edit tools in 2.0.76? If so, what's your configuration?
Reproduction Steps
Expected: Hook invoked, file access denied
Actual: Hook not invoked, file access succeeds
PretoolUse hook is not working for me too -
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node $PWD/hooks/read_hook.js"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node $PWD/hooks/query_hook.js",
"timeout": 300
}
]
},
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "jq . > pre-log.json"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_response.filePath // .tool_input.file_path // empty' | xargs -I {} npx --yes prettier --write {} 2>/dev/null || true"
},
{
"type": "command",
"command": "node $PWD/hooks/tsc.js"
}
]
},
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "jq . > post-log.json"
}
]
}
]
}
}
I had hooks working correctly as of last night, but now I get
PreToolUse:Edit hook errorwith no meaningful error information. The worst part is I have this hook in place as a security precaution, and claude is just continuing on without actually running the hook rather than stopping out of safety.By the way, this also happens on Claude Code on the web
@bcherny Any chance we could get the team to look at this - 4 months old now.
@bcherny Do you have a rough estimate or any insights on when this issue might become more focused or investigated? Should we wait for it, or is it not currently on your plate to actively look into what’s going on?
From our side, none of the prompt-based hooks are working. Additionally, the Claude Code terminal/hooks window layout is completely broken — we can’t edit or delete prompt-based hooks at all
Still experiencing this on Claude Code 2.x (Opus 4.6, macOS + Linux).
Our use case: We manage homelab infrastructure with FortiManager/FortiAnalyzer MCP servers. We need a PreToolUse hook to guard
mcp__fortimanager__create_*calls — requiring user confirmation before creating firewall policies or address objects on production infrastructure.Current workaround: Using a
UserPromptSubmithook instead, but this only fires per-prompt, not per-tool-call. It can't intercept specific MCP tool invocations mid-conversation, which is the whole point of PreToolUse.Impact: Without this, we can't implement tool-level safety guards for destructive MCP operations. The hook system is a great security concept — it just needs to actually fire.
Would appreciate an update on whether this is being tracked internally.
I believe some of the problem might be that "Read" does not appear in the list here: https://github.com/anthropics/claude-code/blob/1718a574950cd8979b27b3e21f5e82760b10e8e0/plugins/hookify/hooks/pretooluse.py#L48
Additional repro: auto-allowed tools skip PreToolUse hooks entirely
Claude Code version: 2.1.77 (Windows 11, Git Bash)
Date: 2026-03-17
Setup
Project-level
.claude/settings.jsonwith both hooks and an allow list:Test
Readtool on a file → hook did NOT fire (no marker file created, noadditionalContextinjected)Writetool on a protected path → hook did NOT fire (write succeeded when it should have been denied)Conclusion
When a tool is in
permissions.allow, the PreToolUse hook pipeline is never invoked. The hooks are correctly configured (/hooksshows them), the scripts execute correctly when run standalone — Claude Code simply never calls them for auto-allowed tools.This makes PreToolUse hooks unusable for any security/guardrail purpose, since the tools you most need to guard (Read, Write, Edit) are typically in the allow list to avoid permission prompts on every operation.
Expected behavior
PreToolUse hooks should fire on all tool invocations regardless of permission status, as documented: "PreToolUse hooks run before the permission system."
Additional data point: PreToolUse on Read matcher confirmed broken (v2.1.79, Windows 11)
Use case: Enforcing TRV (semantic code index) usage before agents read Rust source files. We have a multi-agent pipeline (overseer → planner → implementer) where agents are supposed to use
trv query <symbol> --jsonto get exact file:line before reading code, but telemetry shows 0 TRV commands across 53 subagent sessions despite "MANDATORY" instructions in agent definitions.What works: PreToolUse hooks on
Bashmatcher fire correctly (both in main session andclaude -psubprocesses). Our grep/find/ls/cat blocklist hook has been enforcing reliably.What doesn't work:
"matcher": "Read"— never fires. Tested with 5 parallel subagents + 5claude -pinvocations. 0/10 blocked."matcher": "Bash"— also never fires (added mid-session, but also fails in freshclaude -pprocesses).Config (global
~/.claude/settings.json):Hook script: Works perfectly when invoked directly (
CLAUDE_TOOL_INPUT='...' bash hook.sh→ correct exit 2 with block message). The issue is 100% that Claude Code never invokes it.Impact: We can enforce "don't use grep" via Bash hooks, but we cannot enforce "use TRV before reading code" because Read hooks don't fire. This is the difference between blocking one evasion path vs enforcing the correct workflow.
Workaround attempted: Restricting agents via
tools:frontmatter (removing Grep from planners/implementers). This works for tool-level restrictions but can't express conditional logic like "Read is allowed IF TRV was used first."Correction: PreToolUse Bash matcher also broken (v2.1.79, Windows 11)
Previous comment update: My earlier comment (2026-03-22) stated that PreToolUse hooks on the Bash matcher fired correctly. After a systematic senator review (2026-03-28) with a structured 34-test plan, I need to correct this: PreToolUse hooks do not fire for any matcher, including Bash.
Test setup:
block-grep-for-planners.shwired as PreToolUse Bash hookgrep,find,ls,cat,curl— all executed successfully despite the hookWhat I now believe happened originally: The Bash "allowlist" appeared to work because the allowed commands (
echo,cargo,git,trv) succeeded, and I attributed blocking to the hook rather than testing the actual blocked commands in a live session. The senator review tested all 11 cases systematically and found 0/5 blocked commands were actually intercepted.Updated scope of the bug:
| Matcher | PreToolUse | PostToolUse |
|---------|-----------|------------|
| Bash | broken | broken |
| Read | broken | not tested |
|
*(wildcard) | broken | not tested |Impact: With PreToolUse broken across all matchers, the only working enforcement point is prompt-level instructions in agent definitions, which our telemetry shows is insufficient (0/53 subagent sessions followed TRV-first navigation despite "MANDATORY" instructions).
Workaround under evaluation: PostToolUse warning hooks (which DO fire) as a softer enforcement — log violations and surface warnings rather than blocking. Less effective than PreToolUse blocking but at least provides visibility.
Environment: Claude Code v2.1.79, Windows 11 Pro 10.0.26200, bash shell
Additional data point (Windows 11, v2.1.111, Claude Desktop App): Same claude.exe binary fires PreToolUse correctly via
claude.exe --print --debughooks but never fires it when invoked by the Desktop App (or Web per @jpcaparas).The App launches it with
--output-format stream-json --input-format stream-json— strongly suggests stream-json mode bypasses the PreToolUse pipeline entirely. Settings are being loaded in that session (permissions.denyfrom the same file still blocks tools), so the file is read — just the hooks section is ignored.---
I've spent some time trying to get this to work like my CLI, and we (me/Claude haha) got stuck here.
Independent reproduction confirming @chadsten's hypothesis
Adding an independent reproduction that matches @chadsten's hypothesis (stream-json mode bypassing the PostToolUse pipeline) precisely.
Environment
Observations
log_tool_use.py) that uses neither${CLAUDE_PROJECT_DIR}norshell: "bash". Variable-expansion and shell-spec hypotheses were ruled out by this fact.echo '{...}' | python ~/.claude/hooks/log_tool_use.py) appends to the CSV log as expected. This confirms the failure is on the spawn side (Claude Code), not the script side.${CLAUDE_PROJECT_DIR}expands to empty in the bash subshell that hooks would launch under. We observed this early but ruled it out as the root cause once observation 1 was established.Notes
Confirming on v2.1.119 (CLI, macOS) — with a useful asymmetry data point
Environment
2.1.119(CLI, not Desktop App / Web).claude/settings.json, no~/.claudePostToolUse overrideThe asymmetry that pinpoints the dispatcher
This bug is not "all hooks for Bash are broken." In the same fresh CLI session, with the same
settings.json, against the sameBashtool calls:PreToolUsematcherBashfires reliably (script blocks the gated command with stderr/exit 2)PostToolUsematcherBashnever fires (script never spawned; verified via filesystem trace)Repro (under 30 seconds)
``
bash
`# mark-success.sh (PostToolUse)
echo "$(date +%s) post" >> /tmp/posttool-bash-trace.log
`bash
``# pre-gate.sh (PreToolUse)
echo "$(date +%s) pre" >> /tmp/pretool-bash-trace.log
pretool-bash-trace.loggets a line,posttool-bash-trace.logdoes not.Direct invocation works
Piping the documented JSON shape to the same script writes the marker correctly:
So the failure is unambiguously on the dispatcher / spawn side for the PostToolUse:Bash branch specifically — not the script, not the JSON contract, not the matcher syntax.
Hypothesis check vs. earlier comments
@chadsten2026-04-20 and@satpon-ai2026-05-04) — this is plain CLI, no Desktop App / Web involved.@dazuiba2025-09-04) —permissionsfrom the same file load fine, and the siblingPreToolUse:Bashentry from the same hooks block dispatches correctly.The fact that one entry in the same
hooksarray dispatches and the adjacent entry does not strongly suggests the bug lives in the PostToolUse:Bash branch of the dispatcher specifically (or in how its matcher resolves) — not in settings loading or stream-json mode.Workaround we landed on
Since
PreToolUse:Bashdoes fire, you can returnhookSpecificOutput.updatedInput.commandto append a success-conditional side effect to the original command (<cmd> && <marker-write>). This bypasses the broken dispatcher and uses real exit codes instead of text-pattern parsing oftool_response.stdout. Happy to share the script if useful — but obviously a real fix is preferred sincePostToolUse:Bashis a load-bearing primitive for any commit-gate workflow.cc @bcherny — the asymmetry above might narrow the search.
Supplementary repro: PreToolUse:Bash also drops dispatches under parallel tool batching
While building the
updatedInputworkaround above, I observed a separate dispatcher quirk that's worth the upstream team's awareness. Same Claude Code v2.1.119, same CLI session.Setup
A single PreToolUse:Bash hook (
.claude/hooks/pretool-inject-success-marker.sh) that detectstsc --noEmit/next build/next lintcommands and rewrites them viahookSpecificOutput.updatedInput.commandto append&& date +%s > <abs-marker-path>. Verified working with sequential single-call invocations — every time.Observation
When the assistant batches two Bash tool calls in one response (one assistant message, two
<invoke>blocks), only one of them gets its PreToolUse:Bash hook dispatched. The second silently runs without the rewrite (no marker created on disk despite the underlying command exiting 0).The same hook script. Same matcher. Same session. The only difference is whether the two Bash invocations were dispatched as a single parallel batch or sequentially.
Hypothesis
The dispatcher path that handles parallel tool-call batches doesn't iterate the hooks per-tool-call the same way the sequential path does — likely an enumeration / mutex / promise issue that drops the second call's hook dispatch on the floor. This is consistent with this thread's prior reports of intermittent PostToolUse:Bash dispatching under stream-json mode (
@chadstenand@satpon-aiabove): if both branches share a queue/serialization layer that has a race or short-circuit when more than one event arrives in quick succession, that would explain both the (entirely missed) PostToolUse:Bash and the (partially missed) parallel PreToolUse:Bash behaviors.Workaround applied
In our gate workflow, gate-relevant commands (
tsc,build,lint) are now run sequentially, not in a parallel tool-call batch. That fully restores reliability for our use case. Worth knowing if you're triaging — sequentialization is a reasonable user-facing mitigation, but the dispatcher should probably be fixed to handle parallel batches symmetrically.Another data point: HTTP-typed PostToolUse hook, headless
-p+ stream-jsonMost reports here use
command-typed hooks. Adding a clean repro on the HTTP variant — same dispatcher, same outcome.Setup: orchestrator spawns Claude Code workers via
claude code -p "<prompt>" --dangerously-skip-permissions --output-format stream-json. Per-worker~/.claude/settings.json:What works:
enabledPluginsfrom the same file applies normally.curl -X POSTfrom inside the same container with the same headers returns 204 and persists.What doesn't:
Matches @chadsten / @satpon-ai / @danielEuc95 hypothesis: the dispatcher path under
--output-format stream-jsonskips PostToolUse entirely. The bug is in the dispatcher, not in settings loading or hook config syntax — confirmed across bothcommandandhttphook types.Happy to provide a self-contained repro if useful.
Confirming this on Windows 11 Pro (10.0.26200) with the VS Code Claude Code extension, version 2.1.132. Adds cross-platform evidence — the existing reports (this issue and duplicate #55644) are both macOS.
Working on this install:
SessionStarthook fires reliably (a PowerShell-based weekly maintenance routine).Silent on this install:
PreToolUseandPostToolUsehooks forWriteandEditmatchers. Tested over a multi-day window; zero invocations.Settings.json hook block (canonical shape, parses cleanly, no warnings):
Evidence the hook runner is silent, not the script:
msg.exe(Windows native, with"shell": "powershell"), then a one-lineAdd-Contentto a log file. Both yield zero observable activity across manyEdit/Writeoperations after a full Claude Code restart.Reproduces deterministically across session restarts. Bug spans at least 2.1.119 (per #55644) through 2.1.132 on this install.
**Additional data point: PostToolUse dispatch is partially alive — hooks execute at session start with synthetic/malformed payloads, but never for real Edit/Write events (v2.1.173, macOS)**
Setup:
PostToolUsehook wired in~/.claude/settings.jsonwith matcherWrite|Edit→ a Python hook script instrumented with a trace log that records every invocation of the script, including early exits.Observed on Claude Code 2.1.173 (native binary, macOS), continuing the regression we have tracked since 2.1.119:
Note
"file_path": 42— an integer — and two payloads that are not dicts at all. These look like a startup schema probe/self-test rather than real tool events.shutil.copy2+ SHA256 verification) for ~60 consecutive working sessions.Diagnostic implication: the dispatch layer can spawn wired hook processes (the session-start burst proves exec + stdin plumbing work), but real tool events are never routed to PostToolUse hooks — i.e., this looks like an event-wiring failure rather than a dead dispatcher. That distinction may help localize the bug.
Happy to provide the instrumented hook script or fuller logs if useful.
Disclosure: this report was prepared with an AI agent (Claude Code v2.1.173 / Claude Fable 5) operating an instrumented hook setup; the trace records above are verbatim from the instrumentation log.
Still reproducing on Claude Code 2.1.153 / Opus 4.7 (1M context), macOS Darwin 25.5.0 (Sequoia 15).
Use case: I tried to wire a
PreToolUsehook on theBashmatcher to blockgit commitinvocations whose message contains forbidden trailers (Co-Authored-By: Claudeetc.) — i.e. exactly the kind of guardrail this hook layer is documented to support. The hook is configured correctly in~/.claude/settings.json(other hooks in the same file work:SessionStartfires reliably every turn,UserPromptSubmitfires every prompt).PreToolUsewith the same configuration never fires — same symptom as the original report.Downstream impact: this directly compounds with #19471 (CLAUDE.md instructions deprioritized after compaction). The user-facing failure mode is: documented rules in
preferences.md/CLAUDE.mdare not reliably honored mid-session, AND the hook layer that should backstop them does not fire. As a result, real users are falling back to git-levelcommit-msghooks and server-side CI checks (i.e. enforcement layers entirely outside Claude Code) to enforce guardrails the harness should support natively.Would appreciate an ETA or a workaround — right now the only documented hook surface that I can use for blocking is ineffective.
Still reproducing this on Claude Code 2.1.197 / Sonnet 4.5 / macOS Darwin 26.5.2 (Tahoe)
How's this PreToolUse not fixed... Claude Desktop App 1.20186.1
Heh, Fable fixed it for me when I bruteforced it with it, can't remember prompts, GG!
EDIT Think it had to do with Python maybe lol
Confirming this on 2.1.211 (macOS). Adding a data point since I isolated it pretty specifically:
PostToolUsehook with matcher"Bash", configured in project-level.claude/settings.json./hookscorrectly shows it registered — drilling intoPostToolUselists it separately by matcher ([Project] Bash), alongside two other workingWrite/Write|Editproject hooks and a global[User] (all)hook from~/.claude/settings.json. All four show up correctly and don't interfere with each other in the listing.{"tool_input": {"command": "..."}}) works perfectly — correctsystemMessageJSON output, exit 0./hooksreload and after a full app restart, same result both times.So in my case it's specifically isolated to: config is valid, registration is correct and confirmed via
/hooks, the hook script itself is correct and verified in isolation — but the runtime event never actually invokes it during a real Bash tool call. Matches what's described here (recognized but never fired), for whatever it's worth as an additional confirmation.