Async hook stdout parser fails on multi-line JSON, silently drops additionalContext
Bug Description
Async hooks that output multi-line (pretty-printed) JSON to stdout have their output silently discarded. The async hook stdout parser processes output line-by-line, finds the opening { on its own line, tries to JSON.parse("{"), fails, and creates an empty attachment {} — dropping the hook's additionalContext entirely.
This affects any async hook (e.g., SessionStart with "async": true) that outputs formatted JSON. Synchronous hooks are not affected — they appear to use a different parsing code path.
Reproduction
- Create an async
SessionStarthook that outputs multi-line JSON:
// settings.json
{
"hooks": {
"SessionStart": [{
"matcher": "startup|resume|clear|compact",
"hooks": [{
"type": "command",
"command": "~/.claude/hooks/test-hook.sh",
"async": true
}]
}]
}
}
#!/usr/bin/env bash
# test-hook.sh — outputs pretty-printed JSON (the default for heredocs, jq, etc.)
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "This context will be silently dropped"
}
}
EOF
- Start a new session
- Check the debug log
Expected
The additionalContext string is injected into the model's context.
Actual
Debug log shows:
Hooks: Processing 7 lines of stdout for async_hook_25727
Hooks: Found JSON line: {...
Hooks: Failed to parse JSON from async_hook_25727: {
Hooks: Creating attachment for async_hook_25727 (SessionStart:startup): {}
The parser finds line 1 ({), tries to parse just { as a complete JSON document, fails, and creates an empty attachment {}. The additionalContext is silently discarded with no user-visible error.
Workaround
Pipe the hook's output through jq -c to produce single-line compact JSON. The line-by-line parser can then parse the entire document as a single "JSON line":
# This succeeds:
original-hook.sh | jq -c .
# Output: {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"..."}}
Debug log with single-line JSON:
Hooks: Processing 2 lines of stdout for async_hook_30135
Hooks: Found JSON line: {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"<EXTREMELY_IMPORTANT>\nYo...
Hooks: Creating attachment for async_hook_30135 (SessionStart:startup): {"hookSpecificOutput":{...full content...}}
Real-World Impact
The superpowers plugin (available in the Claude Code Plugin Marketplace) defines an async SessionStart hook that injects skill usage instructions via additionalContext. Due to this bug, those instructions are silently dropped in every session, making the plugin's core skill auto-invocation mechanism non-functional.
The plugin's session-start.sh outputs well-formed but pretty-printed JSON via a bash heredoc — a perfectly reasonable thing to do. There is no documentation indicating that async hook stdout must be single-line JSON.
Suggested Fix
The async hook stdout parser should accumulate all lines and parse the complete output as a single JSON document, rather than parsing individual lines. This would match how synchronous hooks appear to work and would be consistent with standard JSON (which permits whitespace/newlines).
Environment
- Claude Code version: 2.1.38
- OS: macOS (Darwin 25.2.0)
- Affected plugin: superpowers v4.2.0 (but any async hook with multi-line JSON is affected)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗