PreToolUse hooks don't fire on user slash commands; PostToolUse does

Resolved 💬 5 comments Opened Apr 1, 2026 by mbriggsy Closed May 23, 2026

Summary

PreToolUse hooks with matcher "Skill" do not fire when the user types a slash command (e.g., /ce:work). PostToolUse hooks with the same matcher do fire. This is undocumented and creates a significant gap in hook-based enforcement workflows.

Additionally, two related findings about hook output delivery:

  1. Only error-path output reaches the model{"decision":"block"} (exit 0) and stderr (exit 2) deliver. All non-blocking formats (systemMessage, decision:allow+reason, plain text, hookSpecificOutput, continue+systemMessage) are silently discarded. This applies to both PreToolUse and PostToolUse.
  1. PostToolUse + {"decision":"block"} delivers the message WITHOUT undoing the tool result — the tool's output survives, and the block reason is shown to the model. This is useful but undocumented.

Reproduction: PreToolUse doesn't fire on user slash commands

Setup

~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Skill",
        "hooks": [{
          "type": "command",
          "command": "bash ~/.claude/hooks/test-pretool.sh"
        }]
      }
    ]
  }
}

~/.claude/hooks/test-pretool.sh:

#!/bin/bash
INPUT=$(cat)
SKILL=$(echo "$INPUT" | jq -r '.tool_input.skill // empty')
case "$SKILL" in
  ce:work|ce-work|compound-engineering:ce-work)
    touch /tmp/.pretool-hook-fired
    cat <<'EOF'
{"decision": "block", "reason": "TEST: PreToolUse block on ce:work"}
EOF
    ;;
  *) exit 0 ;;
esac

Test 1: User types slash command

User types: /ce:work test
Result: Skill loads normally. No block message. /tmp/.pretool-hook-fired does NOT exist.

The hook never ran. Not "ran but block was ignored" — the script never executed (verified via side-effect file creation).

Test 2: Claude invokes Skill tool programmatically

# Claude uses: Skill(skill="compound-engineering:ce-work", args="test")
Result: Tool is blocked. Block message delivered. /tmp/.pretool-hook-fired EXISTS.

The hook fires and blocks correctly when Claude invokes the Skill tool.

Test 3: PostToolUse fires on user slash commands

User types: /brief
Result: PostToolUse hook fires. Block message delivered as system-reminder.

PostToolUse hooks DO fire on user slash commands. Only PreToolUse is skipped.

Reproduction: Non-blocking output silently discarded

Tested 7 output formats on PostToolUse (same results on PreToolUse):

| # | Format | Exit | Delivered? |
|---|--------|------|-----------|
| 1 | plain text stdout | 0 | No |
| 2 | {"systemMessage":"..."} | 0 | No |
| 3 | {"decision":"allow","reason":"..."} | 0 | No |
| 4 | {"hookSpecificOutput":{"permissionDecision":"allow"},"systemMessage":"..."} | 0 | No |
| 5 | {"continue":true,"systemMessage":"..."} | 0 | No |
| 6 | stderr text | 2 | Yes |
| 7 | {"decision":"block","reason":"..."} | 0 | Yes |

Only error-path outputs (exit 2 + stderr, or decision:block) reach the model.

Reproduction: PostToolUse block preserves tool result

// PostToolUse hook returns:
{"decision": "block", "reason": "TEST — did this reach Claude?"}

Result: The block message appears as a system-reminder with prefix PostToolUse:Skill hook blocking error. The skill's output (SKILL.md content) is fully preserved in the conversation. The "block" does not undo or discard the tool result.

Expected behavior

  1. PreToolUse hooks should fire on user slash commands the same way they fire on Claude's Skill tool invocations
  2. Non-blocking hook output (systemMessage, decision:allow+reason) should be delivered to the model, not silently discarded
  3. The behavior of PostToolUse + block (message delivered, result preserved) should be documented

Impact

  • PreToolUse gap makes it impossible to enforce workflows before user-initiated skills (e.g., "run /brief before /ce:work")
  • Non-blocking output gap (7+ existing issues: #19432, #18534, #25987, #24788, #20062) prevents context injection, advisory messages, and workflow reminders
  • Undocumented PostToolUse block behavior is actually useful but only discoverable by accident

Environment

  • Claude Code CLI on Windows 11 (Git Bash)
  • Tested 2026-04-01
  • All hooks use type: "command" with bash scripts

View original on GitHub ↗

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