Post/PreToolUse Hooks Not Executing in Claude Code

Open 💬 36 comments Opened Aug 22, 2025 by fwends
💡 Likely answer: A maintainer (dicksontsai, collaborator) responded on this thread — see the highlighted reply below.

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:

  1. Verified hook scripts execute correctly when run manually
  2. Tested multiple matchers: "*", "Bash", "Task", "Write", "Edit"
  3. Tested both simple commands and complex scripts
  4. Confirmed proper JSON input handling with jq parsing
  5. Verified file permissions and absolute paths
  6. Cleared logs and performed fresh tests
  7. 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.

View original on GitHub ↗

36 Comments

coygeek · 10 months ago

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

  1. Snapshot at Startup: When a Claude Code session begins, it takes a snapshot of all hooks from your settings files (~/.claude/settings.json, .claude/settings.json, etc.).
  2. Locked During Session: Once the session starts, this configuration is locked and does not automatically update, even if you edit the settings files on disk.
  3. Active Monitoring and Warnings: Claude Code monitors your settings files for changes. If it detects that hooks have been modified externally during your session, it will display a warning, prompting you to review the changes.
  4. Explicit Application Required: To protect your session, changes only take effect when you explicitly apply them using one of the two methods described below.

Why This Exists

This security model is designed to protect against:

  • Malicious hook injection into an active, trusted session.
  • Unauthorized code execution through externally modified hooks.
  • Data exfiltration attempts via compromised hooks.
  • Accidental security breaches from applying unreviewed changes.

Common Scenarios and Symptoms

You may have encountered this feature if:

  • You created or modified a hook, but it didn't activate in your current session.
  • An existing hook continued to run with its old behavior after you edited it.
  • You saved changes to settings.json and 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 (/hooks menu)

This is the best practice for updating hooks without disrupting your workflow, as it preserves your current conversation context.

  1. Create or modify your hooks in the appropriate settings.json file and save it.
  2. You may see a warning in Claude Code that your configuration has changed.
  3. Run the /hooks command in your active Claude Code session.
  4. The menu will show you the pending changes, often as a "diff" comparing the old and new hook configurations.
  5. Follow the prompts to review and confirm the changes.
  6. Once confirmed, the new hook configuration will be applied to your current 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.

  1. Create or modify your hooks in the settings.json file.
  2. Exit your current Claude Code session (Ctrl+D or the exit command).
  3. Start a new Claude Code session by running 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

# 1. Create your test directory and hook configuration FIRST
mkdir hook-test && cd hook-test
mkdir -p .claude
cat > .claude/settings.json << 'EOF'
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "*",
      "hooks": [{"type": "command", "command": "echo 'Hook fired!' >> hook.log"}]
    }]
  }
}
EOF

# 2. NOW, start your Claude Code session
claude

# 3. Test your hooks. They will work as expected.
# Claude will read the configuration on startup.

Troubleshooting and Important Considerations

Troubleshooting Checklist

If your hooks aren't working as expected:

  • [ ] Did you apply the changes using the /hooks menu or by restarting your session?
  • [ ] Is your settings.json file valid JSON? (Verify with jq . .claude/settings.json)
  • [ ] Do your hook scripts have execute permissions? (Use chmod +x your-script.sh)
  • [ ] Is your Claude Code version 1.0.38 or newer? (Hooks were released in this version).
  • [ ] Have you checked for conflicts due to settings precedence?

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:

  1. Enterprise Managed Settings (e.g., /etc/claude-code/managed-settings.json on Linux)
  2. Project-level Settings (.claude/settings.json and .claude/settings.local.json)
  3. User-level Settings (~/.claude/settings.json)

Settings from a higher-precedence file will override those from a lower-precedence file.

What DOESN'T Apply Hook Changes

  • Starting a new tmux window or pane.
  • Opening a new terminal tab.
  • Running source ~/.bashrc or 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

  1. Configure First: Set up project-level hooks in .claude/settings.json before starting a major work session.
  2. Test in Isolation: Use a separate test directory to verify new hooks before adding them to your main workflow.
  3. Version Your Hooks: Commit project-level hooks and their associated scripts to version control so the whole team benefits.
  4. Document Your Hooks: Use comments within your hook scripts and your project's README to explain what each hook does.

Summary

The New Golden Rule: Hook changes must be explicitly applied. Use the /hooks menu 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.

coygeek · 10 months ago

Can you retest with this in mind?

Because its working for me.

dicksontsai collaborator · 10 months ago

Your configuration is also working for me.

The new session will take a fresh snapshot of your settings files, and your updated hooks will be active immediately.

Yes, as a security feature, changes to Settings JSON files don't take effect immediately. You either have to acknowledge with /hooks or start a new session.

fwends · 10 months ago

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

Dmdv · 10 months ago

I can confirm this issue persists. I've extensively tested hooks following the documentation exactly:

Environment

  • Claude Code Version: 1.0.89
  • Platform: macOS Darwin 24.6.0
  • Multiple sessions tested over several days

Testing Performed

  1. Created hooks that read JSON from stdin as per documentation
  2. Verified hooks work perfectly when called manually with correct JSON input
  3. Used /hooks command multiple times to apply changes
  4. Restarted sessions dozens of times
  5. Tested with simple echo commands and complex markdown formatters
  6. Verified JSON validity, file permissions, and paths

Evidence

  • Manual execution: echo '{"tool_input": {"file_path": "/tmp/test.md"}}' | ./hook.sh ✅ WORKS
  • Automatic execution via Claude Code tools: ❌ NEVER FIRES
  • No logs created, no output shown, no evidence of execution
  • Tested PreToolUse and PostToolUse with Write, Edit, MultiEdit matchers

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

danieltty · 10 months ago

yes pretooluse hook not working for me too. tried adding sound whenever it asks user for decision, but it never triggers.

Xopher00 · 10 months ago

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.

dazuiba · 10 months ago

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

matssun · 10 months ago

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:

  • SessionStart hooks: Work perfectly (environment setup, weekly checks)
  • PostToolUse hooks: Work perfectly (dependency sync, file processing)
  • PreToolUse hooks: NEVER execute (critical security failure)

🚨 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:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "cd \"$CLAUDE_PROJECT_DIR\" && \"$CLAUDE_PROJECT_DIR/.venv-uv/bin/python\" \"$CLAUDE_PROJECT_DIR/infrastructure/code-standards/hooks/prevent_violations.py\" --tool-name=\"$TOOL_NAME\" --tool-args=\"$TOOL_ARGS\"",
            "description": "Prevent Claude from using dangerous bulk operations and mass editing tools"
          }
        ]
      }
    ]
  }
}

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 perfectly
Claude Code trigger: ❌ Never executes

🔬 Additional Technical Evidence

Configuration Validation

  • ✅ JSON syntax validated with jq
  • ✅ File permissions confirmed (chmod +x on all scripts)
  • ✅ Path resolution tested (all paths absolute and accessible)
  • ✅ Environment variables available ($CLAUDE_PROJECT_DIR set 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

# Test 1: Manual execution (WORKS)
echo '{"tool_name": "Write", "tool_args": "test"}' | ./prevent_violations.py

# Test 2: Claude Code execution (FAILS)
# - Used /hooks command 15+ times
# - Restarted sessions 20+ times  
# - No logs created, no output shown, no evidence of execution

💼 Enterprise Requirements

As an enterprise team, we cannot use Claude Code in production until this is fixed because:

  1. Compliance Risk: We need preventive controls for audit requirements
  2. Code Safety: Bulk operations without validation risk data corruption
  3. Team Consistency: All team members need the same protective measures
  4. Security Policy: Our security framework requires pre-execution validation

🎯 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):

  1. Variable injection: Are $TOOL_NAME and $TOOL_ARGS properly set in PreToolUse context?
  2. Execution timing: Are PreToolUse hooks executing in the wrong thread/context?
  3. Error handling: Are failed PreToolUse hooks silently crashing the tool pipeline?
  4. Event firing: Is the PreToolUse event actually being triggered by tool execution?

🔄 Workaround Status

Currently using reactive PostToolUse validation instead of preventive PreToolUse blocking, but this is:

  • After-the-fact (damage already done)
  • Incomplete protection (can't prevent, only detect)
  • Not compliant with our security requirements

📞 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:

  • Platform: macOS Darwin 24.6.0
  • Enterprise monorepo: 500+ files
  • Hook types affected: PreToolUse only
  • Security impact: Critical (prevention controls bypassed)
  • Workaround: Reactive validation only (insufficient)
dicksontsai collaborator · 10 months ago

@matssun refer to https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-input

Your usage of $TOOL_NAME and $TOOL_ARGS is incorrect.

coygeek · 10 months ago

@matssun

Hey, thanks for posting such a detailed write-up.

It turns out that PreToolUse hooks don't receive tool information via environment variables like $TOOL_NAME or $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 stdin for a PreToolUse hook looks like this:

{
  "hook_event_name": "PreToolUse",
  "tool_name": "MultiEdit",
  "tool_input": { /*...tool-specific arguments object... */ }
}

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 with jq.

Here is the corrected version of your hook command:

cat | jq -c . | { read JSON_INPUT; \
  TOOL_NAME_FROM_JSON=$(echo "$JSON_INPUT" | jq -r '.tool_name'); \
  TOOL_ARGS_FROM_JSON=$(echo "$JSON_INPUT" | jq -c '.tool_input'); \
  cd "$CLAUDE_PROJECT_DIR" && \
  "$CLAUDE_PROJECT_DIR/.venv-uv/bin/python" \
  "$CLAUDE_PROJECT_DIR/infrastructure/code-standards/hooks/prevent_violations.py" \
  --tool-name="$TOOL_NAME_FROM_JSON" \
  --tool-args="$TOOL_ARGS_FROM_JSON"; \
}

Explanation:

  1. cat | jq -c . | { ... }: This reads the entire JSON object from stdin.
  2. read JSON_INPUT: This stores the JSON in a shell variable.
  3. TOOL_NAME_FROM_JSON=$(...): We use jq to extract the .tool_name string.
  4. TOOL_ARGS_FROM_JSON=$(...): We use jq to extract the entire .tool_input object as a compact JSON string. Your Python script will need to be able to parse this JSON string from the --tool-args argument.

Recommended: Modify the Python Script

An even cleaner solution is to modify your prevent_violations.py script to read directly from stdin, 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:

import sys
import json

try:
    # Read the JSON data from stdin
    hook_data = json.load(sys.stdin)
    tool_name = hook_data.get("tool_name")
    tool_input = hook_data.get("tool_input", {})

    # ... your existing validation logic here ...
    # if is_violating(tool_name, tool_input):
    #   print("Violation reason...", file=sys.stderr)
    #   sys.exit(2) # Exit code 2 blocks the tool

except Exception as e:
    print(f"Error in hook: {e}", file=sys.stderr)
    sys.exit(1) # Non-blocking error

sys.exit(0) # Success

Then your settings.json hook command becomes much simpler:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "cd \"$CLAUDE_PROJECT_DIR\" && \"$CLAUDE_PROJECT_DIR/.venv-uv/bin/python\" \"$CLAUDE_PROJECT_DIR/infrastructure/code-standards/hooks/prevent_violations.py\""
          }
        ]
      }
    ]
  }
}

Hope this helps you get unblocked!

github-actions[bot] · 7 months ago

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.

Elijas · 7 months ago

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

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date +%H:%M:%S) PreToolUse: $(cat | jq -r '.tool_name')\" >> /tmp/hook-test-pre.log"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date +%H:%M:%S) PostToolUse: $(cat | jq -r '.tool_name')\" >> /tmp/hook-test-post.log"
          }
        ]
      }
    ]
  }
}

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

  1. Hooks must read from stdin - The JSON payload is piped to stdin, not passed as environment variables ($TOOL_NAME). Use cat | jq -r '.tool_name' to extract the tool name.
  1. Session restart required - Hook changes don't take effect mid-session. Either run /hooks to apply changes or restart Claude Code.
  1. Version matters - Make sure you're on a recent version (claude --version).

This issue appears to be resolved as of v2.0.61.

peanutlife · 6 months ago

This issue still persists and is a major security loophole.

### Environment

  • Claude Code Version: 2.0.76
  • Platform: macOS (Darwin 24.6.0)
  • Hook Config: PreToolUse with matcher: "*" in ~/.claude/settings.json
  • Binary: Custom hook handler that enforces file access policies

### Configuration
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "/Users/<user>/.local/bin/securityCheck",
"args": ["--hook-check"],
"timeout": 30
}]
}]
}
}

````

  • Hooks configured in ~/.claude/settings.json (user-level)
  • Session restarted after configuration changes (per official hooks guide)
  • No project-level settings conflicts (verified .claude/settings.json is empty)
  • Removed permissions block from .claude/settings.local.json to avoid Issue #14082
  • Valid JSON configuration verified with jq

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:

  1. ✅ Hooks configuration is loading correctly
  2. ✅ Hook system is active and attempting to run
  3. ❌ Hooks fire for some tools (Bash) but NOT others (Read, Write, Edit)

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

  1. Secrets Protection Broken
  • Hook configured to block ~/.securityCheck/secrets/, /.env, ~/.aws/credentials
  • Native Read tool bypasses all protection
  • Secrets accessible despite policy configuration
  1. Trivial Bypass
  • User/agent can simply request "use native Read tool"
  • No hook invocation = no enforcement
  • Security tools built on hooks are non-functional
  1. Enterprise Use Cases Blocked
  • Can't build reliable access control
  • Can't enforce organizational policies

Root Cause Analysis

Two-layer failure:

  1. Hook trigger mechanism broken
  • Hook binary works when called directly but Claude Code never invokes it for native tools
  1. No fallback enforcement
  • When hook fails to run, operation proceeds. It Should fail-closed (deny by default) but currently fails-open (full access)

Workaround

Using MCP layer instead:

  • MCP tools (read_file, execute_command) go through daemon
  • Successfully enforce policies
  • BUT: Easily bypassed by requesting native tools
  • Not a security boundary, just a preference

Ask

For Anthropic Team:

  1. Priority: This blocks security tooling built on Claude Code
  2. Diagnosis: Can you confirm if hook triggers are disabled for certain tools (Read, Write, etc.)?
  3. Timeline: Any ETA on fix for hook system?

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

  1. Create hook handler that logs to file and returns permissionDecision: "deny"
  2. Configure in ~/.claude/settings.json with matcher: "*"
  3. Remove any permissions block from local settings (Issue #14082)
  4. Restart Claude Code
  5. Ask Claude to read any file
  6. Observe: File read succeeds, no hook log entry

Expected: Hook invoked, file access denied
Actual: Hook not invoked, file access succeeds

vigneshsrinivasan9 · 6 months ago

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"
}
]
}
]
}
}

dellis23 · 6 months ago

I had hooks working correctly as of last night, but now I get PreToolUse:Edit hook error with 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.

jpcaparas · 6 months ago

By the way, this also happens on Claude Code on the web

tonydehnke · 6 months ago

@bcherny Any chance we could get the team to look at this - 4 months old now.

Benji1109 · 5 months ago

@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

rstierli · 4 months ago

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 UserPromptSubmit hook 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.

stephan-nordnes-eriksen · 4 months ago

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

AaronHallAttorney · 4 months ago

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.json with both hooks and an allow list:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "node protect-writes.js", "timeout": 10 }]
      },
      {
        "matcher": "Read|Edit|Write|Bash|Glob|Grep",
        "hooks": [{ "type": "command", "command": "node client-work-detect.js", "timeout": 5 }]
      }
    ]
  },
  "permissions": {
    "defaultMode": "default",
    "allow": ["Read", "Edit", "Write", "Bash", "Glob", "Grep"]
  }
}

Test

  1. Used Read tool on a file → hook did NOT fire (no marker file created, no additionalContext injected)
  2. Used Write tool on a protected path → hook did NOT fire (write succeeded when it should have been denied)
  3. Ran the exact same hook script manually with piped JSON → works perfectly (correct JSON output, marker file created, deny decision returned)

Conclusion

When a tool is in permissions.allow, the PreToolUse hook pipeline is never invoked. The hooks are correctly configured (/hooks shows 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."

james-s-usec · 3 months ago

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> --json to 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 Bash matcher fire correctly (both in main session and claude -p subprocesses). Our grep/find/ls/cat blocklist hook has been enforcing reliably.

What doesn't work:

  • PreToolUse hook with "matcher": "Read" — never fires. Tested with 5 parallel subagents + 5 claude -p invocations. 0/10 blocked.
  • PostToolUse hook with "matcher": "Bash" — also never fires (added mid-session, but also fails in fresh claude -p processes).

Config (global ~/.claude/settings.json):

{
  "matcher": "Read",
  "hooks": [{
    "type": "command",
    "command": "bash ~/.claude/hooks/block-untargeted-reads.sh"
  }]
}

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

james-s-usec · 3 months ago

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.sh wired as PreToolUse Bash hook
  • Hook script returns exit 2 + BLOCK message when invoked directly (verified)
  • Tested commands: grep, find, ls, cat, curl — all executed successfully despite the hook

What 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

chadsten · 2 months ago

Additional data point (Windows 11, v2.1.111, Claude Desktop App): Same claude.exe binary fires PreToolUse correctly via claude.exe --print --debug hooks 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.deny from 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.

satpon-ai · 2 months ago

Independent reproduction confirming @chadsten's hypothesis

Adding an independent reproduction that matches @chadsten's hypothesis (stream-json mode bypassing the PostToolUse pipeline) precisely.

Environment

  • OS: Windows 11
  • Claude Code: invoked via Claude Desktop App (native installer, migrated from winget on 2026-04-27)
  • Settings in use: both ~/.claude/settings.json (global) and project-scoped .claude/settings.json

Observations

  1. All hooks in the global PostToolUse array fail to fire. This includes a minimal logger log_tool_use.py) that uses neither ${CLAUDE_PROJECT_DIR} nor shell: "bash". Variable-expansion and shell-spec hypotheses were ruled out by this fact.
  1. Direct stdin invocation of the same hook scripts works correctly. Piping a synthetic tool-use event to the script 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.
  1. ${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.
  1. The permissions block in the same settings.json is honored. Allow/deny rules apply correctly to tool calls, confirming the file itself is being read. Only the hooks section is silently skipped — consistent with a pipeline-bypass interpretation rather than a settings-loading failure.
  1. Behavior matches @chadsten's hypothesis precisely: under stream-json input/output mode (which Claude Desktop App uses to invoke claude.exe), the PostToolUse pipeline appears to be bypassed entirely.

Notes

  • Reproducible across multiple sessions and project directories.
  • We have a working manual workaround in place that does not depend on hook firing, so this is not blocking us — but a fix or official acknowledgment would be welcome.
  • Happy to provide additional details (full settings excerpts, session traces, etc.) on request.
danielEuc95 · 2 months ago

Confirming on v2.1.119 (CLI, macOS) — with a useful asymmetry data point

Environment

  • Claude Code: 2.1.119 (CLI, not Desktop App / Web)
  • macOS 26.2 (Darwin 25.2.0, arm64)
  • Project-level .claude/settings.json, no ~/.claude PostToolUse override

The 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 same Bash tool calls:

  • PreToolUse matcher Bash fires reliably (script blocks the gated command with stderr/exit 2)
  • PostToolUse matcher Bash never fires (script never spawned; verified via filesystem trace)
// .claude/settings.json (excerpt)
{
  "hooks": {
    "PreToolUse":  [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "bash .claude/hooks/pre-gate.sh" }] }],
    "PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "bash .claude/hooks/mark-success.sh" }] }]
  }
}

Repro (under 30 seconds)

  1. Drop a trace into both hook scripts, e.g.:

``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
``

  1. Restart Claude Code (fresh session — settings snapshot taken).
  2. Run any Bash command in the session.
  3. Observe: pretool-bash-trace.log gets a line, posttool-bash-trace.log does not.

Direct invocation works

Piping the documented JSON shape to the same script writes the marker correctly:

bash .claude/hooks/mark-success.sh < <(echo '{"cwd":"'"$(pwd)"'","tool_input":{"command":"<any command>"},"tool_response":{"stdout":"","interrupted":false}}')
# → marker file is created

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

  • Not stream-json-mode-only as some earlier reports framed it (#6305 comments by @chadsten 2026-04-20 and @satpon-ai 2026-05-04) — this is plain CLI, no Desktop App / Web involved.
  • Not a settings-validation issue either (@dazuiba 2025-09-04) — permissions from the same file load fine, and the sibling PreToolUse:Bash entry from the same hooks block dispatches correctly.

The fact that one entry in the same hooks array 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:Bash does fire, you can return hookSpecificOutput.updatedInput.command to 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 of tool_response.stdout. Happy to share the script if useful — but obviously a real fix is preferred since PostToolUse:Bash is a load-bearing primitive for any commit-gate workflow.

cc @bcherny — the asymmetry above might narrow the search.

danielEuc95 · 2 months ago

Supplementary repro: PreToolUse:Bash also drops dispatches under parallel tool batching

While building the updatedInput workaround 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 detects tsc --noEmit / next build / next lint commands and rewrites them via hookSpecificOutput.updatedInput.command to 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).

Sequential (always works):
  Tool call 1: cd apps/web && npx next build  → hook fires → marker created
  Tool call 2: cd apps/web && npx next lint   → hook fires → marker created

Parallel batch (only one gets dispatched):
  Tool call 1: cd apps/web && npx next build  → hook fires → marker created
  Tool call 2: cd apps/web && npx next lint   → hook NOT dispatched, no marker

Repeating the same lint command sequentially right after → hook fires → marker created.

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 (@chadsten and @satpon-ai above): 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.

PettHa · 2 months ago

Another data point: HTTP-typed PostToolUse hook, headless -p + stream-json

Most 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:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "*",
      "hooks": [{
        "type": "http",
        "url": "$ORCHESTRATOR_BASE_URL/api/v1/hooks/progress",
        "timeout": 5,
        "headers": { "Authorization": "Bearer $ORCHESTRATOR_API_TOKEN", "X-Run-Id": "$RUN_ID" },
        "allowedEnvVars": ["ORCHESTRATOR_API_TOKEN", "ORCHESTRATOR_BASE_URL", "RUN_ID"]
      }]
    }]
  }
}

What works:

  • Settings file IS loaded — enabledPlugins from the same file applies normally.
  • Endpoint IS reachable — manual curl -X POST from inside the same container with the same headers returns 204 and persists.

What doesn't:

  • 0 hook hits across 80+ runs. No traffic in Claude Code's debug log, no traffic at the orchestrator.

Matches @chadsten / @satpon-ai / @danielEuc95 hypothesis: the dispatcher path under --output-format stream-json skips PostToolUse entirely. The bug is in the dispatcher, not in settings loading or hook config syntax — confirmed across both command and http hook types.

Happy to provide a self-contained repro if useful.

stephanpark · 2 months ago

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: SessionStart hook fires reliably (a PowerShell-based weekly maintenance routine).

Silent on this install: PreToolUse and PostToolUse hooks for Write and Edit matchers. Tested over a multi-day window; zero invocations.

Settings.json hook block (canonical shape, parses cleanly, no warnings):

"hooks": {
  "PreToolUse": [
    {"matcher": "Write", "hooks": [{"type": "command", "command": "node ./scripts/file_arbiter.cjs Lock"}]},
    {"matcher": "Edit",  "hooks": [{"type": "command", "command": "node ./scripts/file_arbiter.cjs Lock"}]}
  ],
  "PostToolUse": [
    {"matcher": "Write", "hooks": [{"type": "command", "command": "node ./scripts/file_arbiter.cjs Unlock"}]},
    {"matcher": "Edit",  "hooks": [{"type": "command", "command": "node ./scripts/file_arbiter.cjs Unlock"}]}
  ]
}

Evidence the hook runner is silent, not the script:

  • Hook script invoked manually from a terminal works correctly (sentinel write + log line).
  • Replaced the script with the simplest possible probes — first msg.exe (Windows native, with "shell": "powershell"), then a one-line Add-Content to a log file. Both yield zero observable activity across many Edit / Write operations after a full Claude Code restart.
  • A separate Node-based filesystem watcher reacts correctly to the same file events Claude Code is producing, confirming the underlying writes are happening — only the hook layer is silent.

Reproduces deterministically across session restarts. Bug spans at least 2.1.119 (per #55644) through 2.1.132 on this install.

dwynndev · 1 month ago

**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: PostToolUse hook wired in ~/.claude/settings.json with matcher Write|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:

  1. The hook script DOES get executed at session start — 4 invocations within ~700 ms, but with malformed/synthetic payloads:
{"timestamp": "2026-06-11T23:18:07.018639", "tool_name": "Write", "file_path": "", "exit_reason": "non_dict_tool_input"}
{"timestamp": "2026-06-11T23:18:07.056788", "tool_name": "Write", "file_path": 42, "exit_reason": "no_file_path"}
{"timestamp": "2026-06-11T23:18:07.664332", "tool_name": "", "file_path": "", "exit_reason": "non_dict_payload"}
{"timestamp": "2026-06-11T23:18:07.703468", "tool_name": "", "file_path": "", "exit_reason": "non_dict_payload"}

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.

  1. Zero invocations for genuine Edit/Write tool calls — across full working sessions with dozens of real Edit/Write events, the trace log records nothing after the session-start burst. We have needed a manual fallback (Python 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.

rstierli · 1 month ago

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 PreToolUse hook on the Bash matcher to block git commit invocations whose message contains forbidden trailers (Co-Authored-By: Claude etc.) — 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: SessionStart fires reliably every turn, UserPromptSubmit fires every prompt). PreToolUse with 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.md are 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-level commit-msg hooks 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.

annabarnes1138 · 6 days ago

Still reproducing this on Claude Code 2.1.197 / Sonnet 4.5 / macOS Darwin 26.5.2 (Tahoe)

Dravic · 5 days ago

How's this PreToolUse not fixed... Claude Desktop App 1.20186.1

SHHSSH · 5 days ago

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

dijanalazovska · 5 hours ago

Confirming this on 2.1.211 (macOS). Adding a data point since I isolated it pretty specifically:

  • PostToolUse hook with matcher "Bash", configured in project-level .claude/settings.json.
  • /hooks correctly shows it registered — drilling into PostToolUse lists it separately by matcher ([Project] Bash), alongside two other working Write/Write|Edit project 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.
  • Piping the hook script the exact stdin JSON it would receive ({"tool_input": {"command": "..."}}) works perfectly — correct systemMessage JSON output, exit 0.
  • But running the actual matching Bash command through a real tool call in a live session produces nothing — no system message surfaced to the model, nothing visible in the UI. Tried this both immediately after a /hooks reload 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.