PreToolUse hooks do not fire in non-interactive mode (claude -p)
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
- Create a
PreToolUsehook in.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/hook.sh"
}
]
}
]
}
}
- Hook script that logs and blocks:
#!/bin/bash
echo "HOOK_FIRED" >> /tmp/claude-hook-test.log
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"block"}}'
exit 2
- 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.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗