Skip to content

[Bug] Stop hooks with exit code 2 fail to continue when installed via plugins #10412

@kylesnowschwartz

Description

@kylesnowschwartz

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:

  1. Exits with code 2
  2. Outputs JSON to stderr containing "decision": "block" and a "reason" field

Claude Code should:

  1. Parse the JSON from stderr
  2. Extract the reason field
  3. Feed the reason to Claude as continuation instructions
  4. 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:

  1. Hook executes and returns exit code 2 with JSON to stderr
  2. Claude Code displays: ⏺ Stop hook prevented continuation
  3. 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 reason field

Environment

  • Platform: macOS
  • Claude Code Version: 0.3.10
  • Hook Implementation: Uses claude_hooks Ruby 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: gabriel-dehan/claude_hooks#11

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:corebugSomething isn't workinghas reproHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOS

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions