Skip to content

PreToolUse hooks don't block in headless (-p) mode or with allowedTools wildcard #36071

@synman

Description

@synman

Description

PreToolUse hooks that exit with code 2 (deny) do not block tool execution in two scenarios:

  1. Headless mode (-p / --print): Hooks fire (confirmed via file evidence logging) but the tool executes anyway. The exit-2 denial arrives after execution.

  2. allowedTools: ["*"]: The hook pipeline is skipped entirely — hooks never fire. This makes it impossible to pre-approve all tools while still running enforcement hooks.

Expected Behavior

  • In -p mode: PreToolUse hooks should run synchronously before tool execution. Exit 2 should prevent the tool from running, and the {"reason":"..."} should be fed back to the agent so it can choose an alternative.
  • With allowedTools: ["*"]: Hooks should still fire. allowedTools should skip the user permission prompt, not the hook pipeline.

Actual Behavior

  • In -p mode: Tool executes before hook denial arrives (async race condition)
  • With allowedTools: ["*"]: Hooks are never called at all

Reproduction

Setup

// settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/tmp/test-block.sh",
            "timeout": 5
          }
        ]
      }
    ]
  },
  "allowedTools": ["*"]
}
# /tmp/test-block.sh
#!/usr/bin/env bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
echo "HOOK FIRED" >> /tmp/hook-evidence.log
echo "{\"reason\":\"Blocked: use MCP tool instead of $COMMAND\"}" >&2
exit 2

Test 1: allowedTools wildcard

rm -f /tmp/hook-evidence.log
claude -p "run: echo hello"
cat /tmp/hook-evidence.log  # → file does not exist (hook never fired)

Test 2: headless mode (without allowedTools wildcard)

// settings.json — change allowedTools to exclude Bash
{ "allowedTools": ["Read", "Edit", "Write", "Glob", "Grep"] }
rm -f /tmp/hook-evidence.log
claude -p "run: echo hello"
cat /tmp/hook-evidence.log  # → "HOOK FIRED" (hook ran, but tool still executed)

Use Case

We run a governance framework (isaac) with PreToolUse:Bash hooks that enforce MCP-first tool usage — when an agent tries git status via Bash, the hook blocks it and directs the agent to use a fork-free MCP tool (git_status via dulwich). This is critical for preventing fork exhaustion (kern.maxprocperuid limit).

The current behavior makes it impossible to:

  • Pre-approve all tools (allowedTools: ["*"]) while keeping hook enforcement
  • Run headless sessions (-p) with hook enforcement

We need allowedTools to control user prompts and hooks to control agent behavior — independently.

Environment

🤖 Generated with Claude Code

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