PreToolUse hooks don't fire on user slash commands; PostToolUse does
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:
- 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.
- 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
- PreToolUse hooks should fire on user slash commands the same way they fire on Claude's Skill tool invocations
- Non-blocking hook output (
systemMessage,decision:allow+reason) should be delivered to the model, not silently discarded - 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
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗