[Bug] Plugin hooks JSON output not captured

Status Fixed / completed
Maintainer reply ✓ Yes — dicksontsai
Activity 15 comments · opened Nov 2, 2025 · closed Nov 6, 2025
💡 Likely answer: A maintainer (dicksontsai, collaborator) responded on this thread — see the highlighted reply below.

Bug Description

Plugin hooks execute successfully but their JSON output is not captured/parsed by Claude Code. Identical hook files work correctly as inline hooks in settings.json.

Root Cause

Stop Plugin hooks' STDOUT is not being captured by Claude Code. The Stop hook executes, generates valid JSON output, but Claude Code skips the output parsing step entirely and jumps directly to checking for async responses.

Evidence from Debug Logs

Working case (inline hook in settings.json) - Lines 236-242:

[DEBUG] Getting matching hook commands for Stop with query: undefined
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 unique hooks for query "no match query" (1 before deduplication)
[DEBUG] Hooks: Checking initial response for async: {"continue":true,"stopReason":"","suppressOutput":true}
[DEBUG] Hooks: Parsed initial response: {"continue":true,"stopReason":"","suppressOutput":true}
[DEBUG] Hooks: Initial response is not async, continuing normal processing
[DEBUG] Successfully parsed and validated hook JSON output

**Broken case (plugin hook from plugins//hooks/ directory) - Lines 1720-1726:*

[DEBUG] Getting matching hook commands for Stop with query: undefined
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 unique hooks for query "no match query" (1 before deduplication)
[DEBUG] Hooks: getAsyncHookResponseAttachments called
[DEBUG] Hooks: checkForNewResponses called
[DEBUG] Hooks: Found 0 total hooks in registry
[DEBUG] Hooks: checkForNewResponses returning 0 responses

Key difference: Plugin hooks skip from "Matched 1 unique hooks" directly to "getAsyncHookResponseAttachments" with:

  • NO "Checking initial response" line
  • NO "Successfully parsed" line

Verification

Hook files are byte-for-byte identical (MD5 verified):

  • Both output valid JSON when tested manually via CLI
  • Both execute successfully (confirmed in hook log files)
  • Both load all dependencies correctly

Hypothesis

Plugin hook STDOUT is either:

  1. Not being captured by Claude Code's hook execution system
  2. Being redirected/piped incorrectly
  3. Being buffered and not flushed before process exit

The issue is specifically in how Claude Code invokes/captures output from plugin hooks vs inline hooks.

Impact

Plugin-based hooks may execute their logic but sometimes fail to influence Claude Code behavior because their output never reaches the hook processor.

Environment:

  • Platform: darwin
  • Terminal: ghostty
  • Version: 2.0.31
  • Feedback ID: 58c126bf-37b7-4403-a4d0-c9abe17ad27e

View original on GitHub ↗

15 Comments

github-actions[bot] · 10 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/3983
  2. https://github.com/anthropics/claude-code/issues/10412
  3. https://github.com/anthropics/claude-code/issues/10225

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

kylesnowschwartz · 10 months ago
kylesnowschwartz · 10 months ago
kylesnowschwartz · 10 months ago

@dicksontsai Seems like you're all over this issue and similar ones recently - would you like me to close this in favor of one of the potential duplicates? Or do you think this is a unique issue because it relates to the difference between plugin functionality of 'inline' hooks (in settings.json) vs. 'plugin' hooks (via plugin marketplace)?

yuuuuww · 10 months ago

I've been following this issue for many days, but no one has addressed it.

yuuuuww · 10 months ago

I've tried defining my own hooks file or using hooks kits pre-packaged by other people in the community, but I still get the same error. According to historical feedback, it seems to have worked fine in version 0.26, but it's been malfunctioning ever since, which seriously affects my experience using the tool. Because the context length of some large models has been reduced, without hooks to force the relevant hints to be displayed, the tokens output has become uncontrollable and completely unusable.

dicksontsai collaborator · 10 months ago

I'm slightly confused about the bug you reported. Is it a functionality issue? I can't repro it because I see:

> 1+1

⏺ 2

> Stop hook feedback:
🔌 PLUGIN HOOK executed successfully from plugins/test-plugin/hooks/
  ⎿  Stop:Callback hook returned blocking error

⏺ Got it - the plugin hook executed successfully. Is there anything specific you'd like me to help you with regarding the plugin hook system or anything
   else?

> Stop hook feedback:
🔌 PLUGIN HOOK executed successfully from plugins/test-plugin/hooks/
  ⎿  Stop:Callback hook returned blocking error

So the plugin is working as expected. If it's only from reading the debug output, well plugin hooks are implemented differently from the original hooks, so the debug output will look different.

kylesnowschwartz · 10 months ago

Thank you @dicksontsai I've updated the reproduction repo to be more accurate using the correct exit 2 code. It should be reproducible for you now.

Steps:

'Inline' hook (working)

  1. Ensure 'inline' stop hook is configured in your local settings.json file and plugin is disabled
  2. Type anything - see the inline hook response:

<img width="481" height="296" alt="Image" src="https://github.com/user-attachments/assets/9fc55c44-689d-435f-aa18-d7e70fe50164" />

'Plugin' hook (not working)

  1. Remove the 'inline' hook config from your settings.json, add the test-plugin marketplace and enable the plugin
  2. Type anything - see the different response:

<img width="485" height="156" alt="Image" src="https://github.com/user-attachments/assets/446a4847-2edc-42d2-80ca-114b312ead5a" />

kylesnowschwartz · 10 months ago

In case it's not clear - inline hooks when prompted with exit code 2 and a block 'reason' will act on that reason as if prompted by the 'reason' field. Plugin hooks when prompted with the same will just stop entirely.

kylesnowschwartz · 9 months ago

I've spent more time last night attempting to build on top of the hook system - and continue to hit this same bug. This is becoming a blocker for my personal projects - I want to distribute my work built with Claude but if the hooks don't obey the exit code specification when used in conjunction with plugins I'm stuck!

The exit code logic is documented here: https://anthropic.mintlify.app/en/docs/claude-code/hooks#exit-code-2-behavior

Here's my production example code: https://github.com/kylesnowschwartz/claude-bumper-lanes/blob/main/bumper-lanes-plugin/hooks/entrypoints/stop.sh

But I think the reproduction repo I've linked earlier demonstrates it more simply. I don't think it's 'user error' because it works perfectly well when using an inline hook, and the behavior only changes when installed via plugins.

dicksontsai collaborator · 9 months ago

You have to choose to use {exit code 2} or {the advanced JSON hook api with exit code 0}, not both.

If you use exit code 2, then whatever you output to stderr becomes the block reason. What's confusing is that we render the block reason differently between regular and plugin hooks, which we can fix.

In both cases, what you want is a git diff like:

$ git diff plugins/test-plugin/hooks/entrypoints/stop.sh
diff --git a/plugins/test-plugin/hooks/entrypoints/stop.sh b/plugins/test-plugin/hooks/entrypoints/stop.sh
index a6ea721..5853af3 100755
--- a/plugins/test-plugin/hooks/entrypoints/stop.sh
+++ b/plugins/test-plugin/hooks/entrypoints/stop.sh
@@ -9,12 +9,9 @@ stop_hook_active=$(echo "$input" | jq -r '.stop_hook_active // false')

 if [ "$stop_hook_active" = "true" ]; then
   # Already triggered once, allow normal stop
-  echo '{"continue":true,"stopReason":"","suppressOutput":true}' >&1
+  echo '{"continue":true,"stopReason":"","suppressOutput":true}'
   exit 0
 fi

-# Output valid JSON response to STDERR (required when exit code is 2)
-echo '{"continue":true,"stopReason":"","suppressOutput":true,"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."}' >&2
-
-# Exit with code 2 to force continuation
-exit 2
+# Output valid JSON response to STDOUT
+echo '{"continue":true,"stopReason":"","suppressOutput":true,"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."}'

So that your hook looks like:

#!/bin/bash
# Simple Stop hook that blocks (forces continuation) - PLUGIN VERSION

# Read input from STDIN (Claude Code passes hook data as JSON)
read -r input

# Check if stop hook is already active (prevents infinite loop)
stop_hook_active=$(echo "$input" | jq -r '.stop_hook_active // false')

if [ "$stop_hook_active" = "true" ]; then
  # Already triggered once, allow normal stop
  echo '{"continue":true,"stopReason":"","suppressOutput":true}'
  exit 0
fi

# Output valid JSON response to STDOUT
echo '{"continue":true,"stopReason":"","suppressOutput":true,"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."}'
dicksontsai collaborator · 9 months ago

I have a PR to address an issue with (_plugin_ Stop hooks + exit code 2) (hence why you saw the inconsistency), but I still recommend that you use the advanced hooks JSON API + exit code 0, since you already have the JSON object you want.

kylesnowschwartz · 9 months ago

Thank you @dicksontsai for getting in the weeds on this issue - I will implement your suggestions and do some testing. Quick note: The differences I'm seeing currently are not purely a rendering issue. I see behavioral differences: inline-hooks using exit code 2 + advanced JSON api _are_ continuing, and the reason prompt is being fed back to Claude which acts on it, but the same architecture pattern within the plugin-hooks are simply stopping with no continuation. Your explanation regarding the mixing makes sense though - I will try it!

kylesnowschwartz · 9 months ago

I can confirm that with your guidance I've got my plugins working again @dicksontsai

To summarize:

  • hooks mixing exit code 2 with the advanced json api had mismatching behavior when used inline or via a plugin. In the former
  • When mixed, inline hooks respected the advanced JSON behavior and prompted Claude with the reason field in the JSON
  • Plugin hooks on the other hand, respected the exit code 2 and stopped continuation completely

The solution is to pick 1 approach, in my use case the advanced API.

I've opened a PR for https://github.com/gabriel-dehan/claude_hooks/pull/15 (the ruby DSL I use to build upon hooks) which I've manually QA'd and found successful. I'll keep this issue open for now, but will close it once I've manually QA'd further and gain confidence.

Thanks again for your attention.

github-actions[bot] · 9 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.