Async hook stdout parser fails on multi-line JSON, silently drops additionalContext

Resolved 💬 2 comments Opened Feb 10, 2026 by IsaacZhangg Closed Mar 11, 2026

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

  1. Create an async SessionStart hook 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
  1. Start a new session
  2. 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)

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗