[Bug] Plugin hooks JSON output not captured
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:
- Not being captured by Claude Code's hook execution system
- Being redirected/piped incorrectly
- 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
15 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Reproduction repo: https://github.com/kylesnowschwartz/plugin-hook-debug?tab=readme-ov-file
possible dup of https://github.com/anthropics/claude-code/issues/9708
@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)?
I've been following this issue for many days, but no one has addressed it.
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.
I'm slightly confused about the bug you reported. Is it a functionality issue? I can't repro it because I see:
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.
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)
<img width="481" height="296" alt="Image" src="https://github.com/user-attachments/assets/9fc55c44-689d-435f-aa18-d7e70fe50164" />
'Plugin' hook (not working)
<img width="485" height="156" alt="Image" src="https://github.com/user-attachments/assets/446a4847-2edc-42d2-80ca-114b312ead5a" />
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.
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.
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:
So that your hook looks like:
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.
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!
I can confirm that with your guidance I've got my plugins working again @dicksontsai
To summarize:
reasonfield in the JSONThe 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.
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.