PreToolUse hook exit code 2 does not block Task tool calls (agents launch despite block)

Resolved 💬 3 comments Opened Feb 19, 2026 by idiolect-ai Closed Apr 18, 2026

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):

  1. Register the hook above as a PreToolUse hook for Bash
  2. Ask Claude to run gh issue create --repo owner/repo --title "test" --body "test"
  3. The hook fires and returns exit 2
  4. Expected: Bash command is blocked, Claude receives only the error
  5. Actual: Command executes, issue is created on GitHub, Claude receives both the hook's block message AND the command output

Steps (Task — original report):

  1. Register the hook as PreToolUse for Task
  2. Ask Claude to send a Task call (e.g., an Explore agent)
  3. 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, additionalContext JSON emitted, exit 2 returned
  • The gh command executed anyway, creating/editing a public GitHub issue
  • Claude received the hook's blocking message as additionalContext alongside the successful command output
  • bypassPermissions mode 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")

View original on GitHub ↗

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