PreToolUse hook exit code 2 does not block Task tool calls (agents launch despite block)
Description
PreToolUse hooks registered for the Task tool matcher fire correctly and return exit code 2, but the Task (subagent) launches and runs to completion anyway. The hook's additionalContext and stderr output are delivered to Claude alongside the agent's actual results, rather than instead of them. Claude then sees both the "blocked" message and the completed results, leading to duplicated work when it re-sends the Task call.
~~This is Task-specific — PreToolUse exit 2 works correctly for Edit, Write, and Grep matchers using the identical hook format (including additionalContext JSON on stdout).~~
Update (March 3, 2026): This also affects Bash tool calls. A PreToolUse hook for Bash that detects gh issue create / gh issue edit, emits additionalContext JSON, writes to stderr, and exits with code 2 does not prevent the command from executing. The gh command runs to completion and creates/edits the issue despite the hook's exit 2. This is the same pattern as the Task bug — the hook fires, the exit code is registered, but the tool call proceeds anyway.
Affected Tools
| Tool | Exit 2 blocks? | Tested |
|------|----------------|--------|
| Task | No | v2.1.47 (original report) |
| Bash | No | v2.2.19 (March 2026) |
| Edit | Untested since Bash finding | Previously worked in v2.1.47 |
| Write | Untested since Bash finding | Previously worked in v2.1.47 |
| Grep | Untested since Bash finding | Previously worked in v2.1.47 |
We have not re-verified Edit/Write/Grep since discovering the Bash bug. It's possible exit code 2 blocking has regressed more broadly, or that it never reliably worked for Bash and Task.
Potential Fix: permissionDecision
The current Claude Code hooks documentation describes a JSON-based blocking mechanism that may work where exit code 2 does not:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Reason shown to Claude"
}
}
This exits with code 0 and uses permissionDecision: "deny" to prevent execution. The docs also note that stdout is ignored when exit code is 2, which means additionalContext JSON emitted alongside exit 2 may not be delivered through the intended channel.
We have not yet tested permissionDecision: "deny" as a workaround. If it works for Task and Bash, that would confirm exit code 2 is the broken path.
Reproduction
settings.json hook registration:
{
"matcher": "Bash",
"hooks": [
{"type": "command", "command": "/path/to/my-pretool-hook.sh"}
]
}
Hook script (minimal reproduction):
#!/usr/bin/env bash
input=$(cat)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"BLOCKED: public GitHub operation"}}'
echo "BLOCKED" >&2
exit 2
Steps (Bash):
- Register the hook above as a PreToolUse hook for
Bash - Ask Claude to run
gh issue create --repo owner/repo --title "test" --body "test" - The hook fires and returns exit 2
- Expected: Bash command is blocked, Claude receives only the error
- Actual: Command executes, issue is created on GitHub, Claude receives both the hook's block message AND the command output
Steps (Task — original report):
- Register the hook as PreToolUse for
Task - Ask Claude to send a Task call (e.g., an Explore agent)
- Actual: Task launches, agent runs to completion despite exit 2
Evidence (Task — original report)
Across 8 sessions over 7 days, every PreToolUse exit 2 for a Task call resulted in the agent running anyway:
| Metric | Value |
|--------|-------|
| Sessions tested | 8 |
| Total BLOCKED audit entries | 19 |
| Agents that ran despite BLOCKED | 19 (100%) |
| Affects single Task calls | Yes |
| Affects parallel Task calls | Yes |
Audit log from one session showing the full sequence:
09:13:00 | BLOCKED | model=<missing> | type=Explore ← PreToolUse exit 2
09:16:17 | MODEL_VIOLATION | type=Explore ← PostToolUse fires (agent completed)
09:19:04 | ALLOWED | model=sonnet | type=Explore ← Claude re-sends (duplicate)
Evidence (Bash — March 2026)
A PreToolUse hook for Bash correctly detected gh issue create and gh issue edit commands. In both cases:
- Hook fired, pattern matched, stderr written,
additionalContextJSON emitted, exit 2 returned - The
ghcommand executed anyway, creating/editing a public GitHub issue - Claude received the hook's blocking message as
additionalContextalongside the successful command output bypassPermissionsmode was active (which may be a contributing factor per #20946)
Environment
- Claude Code: v2.2.19 (Bash test), v2.1.47 (original Task report)
- macOS 26.2 (Darwin 25.2.0)
- Model: claude-opus-4-6
- Shell: bash
- Permission mode: bypassPermissions
Related
- #24327 (PreToolUse exit 2 behavioral issues — different symptom, same hook/Task interaction area)
- #23284 (PreToolUse hook reports error but doesn't block Bash tool execution)
- #3514 (PreToolUse hooks with preventContinuation:true are not blocking tool execution — closed "not planned")
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗