PreToolUse hooks do not fire in non-interactive mode (claude -p)

Resolved 💬 4 comments Opened Mar 29, 2026 by tschuehly Closed Apr 29, 2026

Summary

PreToolUse hooks configured in .claude/settings.json do not fire when running claude -p (pipe/headless mode).

The hooks documentation at https://code.claude.com/docs/en/hooks implies PreToolUse hooks work in non-interactive mode — the PreToolUse decision control section describes using them to satisfy AskUserQuestion and ExitPlanMode requirements in non-interactive mode (-p). But PreToolUse hooks don't actually execute at all in -p mode.

Reproduction

  1. Create a PreToolUse hook in .claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/hook.sh"
          }
        ]
      }
    ]
  }
}
  1. Hook script that logs and blocks:
#!/bin/bash
echo "HOOK_FIRED" >> /tmp/claude-hook-test.log
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"block"}}'
exit 2
  1. Run: echo 'Run bash command: echo hello' | claude -p --permission-mode acceptEdits

Expected: Hook fires, command is blocked (exit 2).
Actual: Hook never fires, command executes. /tmp/claude-hook-test.log is never created.

Tested with both --permission-mode acceptEdits and --permission-mode default — hooks don't fire in either case.

Version

Claude Code 2.1.87, macOS Darwin 24.6.0

Use case

We run an autonomous agent loop (claude -p --permission-mode acceptEdits) and need PreToolUse hooks to enforce command policies (e.g., blocking raw ./gradlew test in favor of a skill that parses JUnit XML). The hooks work correctly in interactive mode but are silently skipped in -p mode.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗