[Bug] Stop hooks with exit code 2 fail to continue when installed via plugins
Summary
Stop hooks that return exit code 2 (to force agent continuation) work correctly when installed in .claude/hooks/ but fail when installed via the plugin system, displaying ⏺ Stop hook prevented continuation and halting instead of continuing.
Expected Behavior
When a Stop hook:
- Exits with code 2
- Outputs JSON to stderr containing
"decision": "block"and a"reason"field
Claude Code should:
- Parse the JSON from stderr
- Extract the
reasonfield - Feed the reason to Claude as continuation instructions
- Resume agent execution
This is the documented behavior and works correctly for hooks in .claude/hooks/.
Actual Behavior (Plugin Hooks Only)
When the same hook is installed via plugins:
- Hook executes and returns exit code 2 with JSON to stderr
- Claude Code displays:
⏺ Stop hook prevented continuation - Agent execution halts instead of continuing
Reproduction Evidence
Test script that proves identical behavior from both hook locations:
\\\`bash
Create test transcript with reflexive agreement pattern
cat > /tmp/test_transcript.json << 'INNER_EOF'
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"You're wrong"}]}}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"You're absolutely right."}]}}
INNER_EOF
Test direct hook
echo '{"session_id":"test","stop_hook_active":false,"transcript_path":"/tmp/test_transcript.json"}' | \
~/.claude/hooks/entrypoints/stop.rb 2>&1
echo "Exit: $?"
Test plugin hook
echo '{"session_id":"test","stop_hook_active":false,"transcript_path":"/tmp/test_transcript.json"}' | \
~/.claude/plugins/marketplaces/simpleclaude/plugins/sc-hooks/hooks/entrypoints/stop.rb 2>&1
echo "Exit: $?"
\\\`
Both produce identical output:
\\\json\
{"continue":true,"stopReason":"","suppressOutput":false,"decision":"block","reason":"I notice I just used a reflexive agreement phrase. Let me provide a more substantive response:\n\nInstead of simply agreeing, let me analyze your point with specific technical reasoning, consider potential edge cases or alternative approaches, and offer constructive insights that build collaboratively on your observation."}
\\
Exit code: 2 (both)
Root Cause
The hook implementation is correct. Claude Code has different code paths for:
- Direct hooks (
.claude/hooks/): Exit 2 → parse stderr → continue ✓ - Plugin hooks (
plugins/): Exit 2 → show error message → halt ✗
The plugin execution path appears to:
- Not read stderr properly, OR
- Not parse the JSON from stderr, OR
- Not extract/use the
reasonfield
Environment
- Platform: macOS
- Claude Code Version: 0.3.10
- Hook Implementation: Uses
claude_hooksRuby gem v0.7.0 - Plugin: SimpleClaude (
sc-hooks)
Workaround
Moving the Stop hook from plugins/sc-hooks/hooks/ to .claude/hooks/ makes it work correctly.
Related
Original discussion: https://github.com/gabriel-dehan/claude_hooks/issues/11
This issue has 9 comments on GitHub. Read the full discussion on GitHub ↗