Skip to content

[BUG] PreToolUse hooks don't block command execution in bypass mode #20946

@coygeek

Description

@coygeek

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

When running Claude Code with --dangerously-skip-permissions, PreToolUse hooks fire but don't block command execution. The command executes immediately while the hook runs asynchronously in the background. By the time the hook returns its denial (exit code 2), the command has already completed.

In my case, a git commit command succeeded despite the PreToolUse hook returning exit code 2 to block it. The hook denial arrived 37 seconds after the command was issued, but the commit had already been created.

What Should Happen?

PreToolUse hooks should block command execution synchronously, regardless of whether --dangerously-skip-permissions is enabled. When a hook returns exit code 2, the command should NOT execute.

The bypass flag should skip permission prompts, not skip synchronous hook execution.

Error Messages/Logs

// 08:42:38 - Command issued
{"ts":"2026-01-26T08:42:38.987Z","content":[{"type":"tool_use","id":"toolu_01TbrVfyWTwbgAxyZzbzR3LF","name":"Bash","input":{"command":"git commit -m \"feat: add linting...\"}}]}

// 08:43:15 - Hook denial arrives (37 seconds later!)
{"ts":"2026-01-26T08:43:15.641Z","content":[{"type":"tool_result","content":"Hook PreToolUse:Bash denied this tool","is_error":true,"tool_use_id":"toolu_01TbrVfyWTwbgAxyZzbzR3LF"}]}

// 08:43:54 - But commit already exists in git
{"ts":"2026-01-26T08:43:54.392Z","content":[{"tool_use_id":"toolu_01VZ8X7xwGGEfTDwq7pbjtLL","type":"tool_result","content":"ae5aec4 feat: add linting infrastructure and strict mode\n..."}]}


Session statistics:
- Total hook denials: 9 (all with `is_error: true`)
- Commits that succeeded despite denials: 5
- All commits were pushed to remote

Steps to Reproduce

  1. Create a PreToolUse hook that takes 10+ seconds to execute:

    {
      "hooks": {
        "PreToolUse": [{
          "matcher": "Bash",
          "hooks": [{
            "type": "command",
            "command": "sleep 10 && exit 2",
            "timeout": 60
          }]
        }]
      }
    }
  2. Launch Claude Code with bypass permissions:

    claude --dangerously-skip-permissions
  3. Ask Claude to run any bash command:

    Run: echo "hello world"
    
  4. Observe:

    • Command executes immediately and outputs "hello world"
    • 10 seconds later, Claude receives "Hook PreToolUse:Bash denied this tool"
    • The denial arrives AFTER the command already completed

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

No response

Claude Code Version

2.1.19

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

Real-World Impact

My PreToolUse hook runs quality checks (shellcheck, ruff, mypy, pytest) before commits. This takes 30-40 seconds. In bypass mode, commits proceed immediately while checks run in the background. By the time checks fail and return exit code 2, the commit is already in git history.

Session Evidence

Full transcript available in:
~/.claude/projects/-Users-user-Desktop--DONE-2026-01-25-cron-health/c2a76979-a3fb-4060-b7dc-5ef75e35dc05.jsonl

Hook Configuration

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "\"$HOME\"/.claude/hooks/validate-code-before-commit/commit-quality-check.sh",
        "timeout": 300
      }]
    }]
  }
}

Root Cause Hypothesis

The --dangerously-skip-permissions flag appears to make the entire tool execution pipeline asynchronous, not just permission prompts. Hooks fire (we see them return denials), but the command doesn't wait for hook completion before executing.

Documentation Gap

The hooks documentation doesn't specify whether PreToolUse hooks are synchronously awaited in bypass mode. This behavior should be either fixed or documented prominently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions