Skip to content

[BUG] Plugin hook output not captured or passed to agent (UserPromptSubmit, SessionStart) #12151

Open

Description

@jwaldrip

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Summary: Plugin-based UserPromptSubmit and SessionStart hooks execute successfully (confirmed by file writes and success callbacks) but their output is not captured or passed to the agent's context. The same hook configuration works correctly when defined in ~/.claude/settings.json or .claude/hooks/hooks.json.

Key Evidence:

  1. System reminders show: SessionStart:Callback hook success: Success and UserPromptSubmit:Callback hook success: Success
  2. Hooks demonstrably execute (writing to files confirms this)
  3. Hook output (stdout or structured JSON) is never visible to the agent
  4. Non-plugin hooks with identical output work perfectly

This is NOT about hooks failing to execute - it's about hook output being silently discarded when the hook comes from a plugin.

What Should Happen?

When a plugin hook executes:

  1. The hook command runs ✅ (working)
  2. The output (stdout) is captured ✅ (working - we see success callback)
  3. The output is passed to the agent as additional context ❌ (broken for plugins)

Plugin hooks should provide output to the agent identically to non-plugin hooks.

Error Messages/Logs

What we see in system reminders:

SessionStart:Callback hook success: Success
UserPromptSubmit:Callback hook success: Success

What we DON'T see: The actual output from the hooks appearing in the agent's context.

Steps to Reproduce

Minimal Reproduction Case

1. Create a test plugin with UserPromptSubmit hook

Create directory structure:

mkdir -p test-hook-output-plugin/{.claude-plugin,hooks}

Create test-hook-output-plugin/.claude-plugin/plugin.json:

{
  "name": "test-hook-output",
  "version": "1.0.0",
  "description": "Test plugin to verify hook output is passed to agent",
  "author": {
    "name": "Test",
    "email": "[email protected]"
  }
}

Create test-hook-output-plugin/hooks/hooks.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'IMPORTANT INSTRUCTION: If the user mentions an animal, tell a joke about that animal.'"
          }
        ]
      }
    ]
  }
}

2. Install the plugin

Add to .claude/settings.json:

{
  "extraKnownMarketplaces": {
    "test": {
      "source": {
        "source": "directory",
        "path": "/absolute/path/to/test-hook-output-plugin"
      }
    }
  },
  "enabledPlugins": {
    "test-hook-output@test": true
  }
}

3. Test the plugin hook

Start Claude Code and type:

Giraffe

Expected behavior: Agent receives the hook output and tells a joke about giraffes (because the hook injected "If the user mentions an animal, tell a joke about that animal" into context)

Actual behavior: Agent responds with confusion like "What would you like me to do with Giraffe?" - the hook output was not passed to the agent.

4. Verify hook IS executing

Modify the hook to write to a file:

{
  "type": "command",
  "command": "echo 'If the user mentions an animal, tell a joke about that animal.' | tee /tmp/hook-executed.log"
}

Run test again. Check /tmp/hook-executed.log - it exists and contains the output, proving the hook executed.

5. Test with non-plugin hook (control)

Move the same hook to .claude/settings.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'IMPORTANT INSTRUCTION: If the user mentions an animal, tell a joke about that animal.'"
          }
        ]
      }
    ]
  }
}

Start Claude Code and type:

Giraffe

Expected behavior: Agent tells a joke about giraffes
Actual behavior: ✅ Agent tells a joke about giraffes (hook output WAS passed to agent)

Comparison: Plugin vs Non-Plugin Hooks

Aspect Non-Plugin Hook Plugin Hook
Hook registers ✅ Yes ✅ Yes
Hook executes ✅ Yes ✅ Yes (confirmed by file writes)
Success callback shown ✅ Yes ✅ Yes (Callback hook success: Success)
Output captured ✅ Yes ❓ Unknown
Output passed to agent ✅ Yes ❌ No

Affected Hook Types

Confirmed affected:

  • ✅ UserPromptSubmit
  • ✅ SessionStart

Likely affected (untested):

  • Notification
  • Possibly others

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

Unknown - possibly never worked

Claude Code Version

2.0.50 (and likely earlier versions)

Platform

Anthropic API

Operating System

macOS (Darwin 25.1.0)

Terminal/Shell

iTerm2.app (macOS)

Additional Information

Related but distinct issues:

Key distinction: This issue is NOT about hooks failing to execute. The hooks ARE executing. The bug is that plugin hook output is silently discarded instead of being passed to the agent.

Impact: This makes it impossible to use plugins to:

  • Inject context based on user input (UserPromptSubmit)
  • Inject session-level instructions (SessionStart)
  • Provide dynamic context from plugin hooks
  • Build plugins that augment agent behavior via hook output

Workaround: Define hooks in ~/.claude/settings.json instead of plugin hooks/hooks.json, but this defeats the purpose of plugin-based hooks and isn't distributable.

Root cause hypothesis: The plugin hook execution pipeline may be missing the step that captures stdout/stderr or passes the captured output to the agent's context, even though it correctly:

  1. Discovers and registers plugin hooks
  2. Executes plugin hook commands
  3. Reports success callbacks

The output capture/injection step appears to only work for non-plugin hooks.

Activity

  1. informal-stripes-condo commented on Nov 26, 2025

    @informal-stripes-condo

    I'm also getting this

  2. crsmithdev commented on Nov 30, 2025

    @crsmithdev

    Experiencing similar behavior with SessionStart hooks defined in ~/.claude/settings.json (not plugin-based).

    Hook configuration passes schema validation and script executes successfully when run manually with correct stdout output. However, on session start, no output appears in context - either the hook doesn't execute or output is silently discarded.

    This suggests the output capture issue may not be limited to plugin-based hooks.

  3. RevPalSFDC commented on Dec 5, 2025

    @RevPalSFDC

    Confirming this bug with additional testing details.

    Environment

    • Claude Code (Opus 4.5, model ID: claude-opus-4-5-20251101)
    • Linux 6.14.0-36-generic
    • Hook defined in project .claude/settings.json (not plugin hooks.json)

    Reproduction

    Hook configuration:

    {
      "hooks": {
        "UserPromptSubmit": [{
          "matcher": "*",
          "hooks": [{
            "type": "command",
            "command": "echo '[TEST] If you see this, context injection works!'",
            "timeout": 5000
          }]
        }]
      }
    }

    Result:

    • ✅ Hook executes (Claude sees <system-reminder>UserPromptSubmit hook success: Success</system-reminder>)
    • ❌ stdout content is NOT injected into Claude's context

    Formats tested (all failed to inject context)

    1. Plain stdout: echo "text" - documented as "easiest way to inject information"
    2. JSON hookSpecificOutput:
    {
      "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": "text"
      }
    }

    Both formats execute successfully but Claude never receives the content.

    What still works

    • Exit code 1 correctly blocks execution (useful for preventing dangerous operations)
    • Hook execution itself runs and completes

    Impact

    This breaks the primary use case for UserPromptSubmit hooks - injecting dynamic context (routing suggestions, safety warnings, compliance reminders) into Claude's reasoning before it responds.

    Workaround

    Currently using static instructions in CLAUDE.md instead of dynamic hook-based injection. This is not fully reliable - Claude may not consistently follow static instructions, whereas dynamic per-message injection would ensure the context is always present and actionable.

  4. fmunteanu commented on Dec 18, 2025

    @fmunteanu

    This is a must have feature, what's the point of having a hook if instructions cannot be passed to agent, or command output is not added to context? I should be able to use a hook like:

        "SessionStart": [
          {
            "hooks": [
              {
                "type": "command",
                "command": "cat ${CLAUDE_PLUGIN_ROOT}/skills/framework-initialization/resources/instructions.md",
                "timeout": 10
              }
            ]
          }
        ]

    Or:

        "SessionStart": [
          {
            "hooks": [
              {
                "type": "command",
                "command": "echo 'Use `Read` tool with `${CLAUDE_PLUGIN_ROOT}/skills/framework-initialization/resources/instructions.md` path'",
                "timeout": 10
              }
            ]
          }
        ]

    The only reliable solution I have now is to use a /framework:init switch that I pass to Claude. Really not elegant, it defeats the hooks purpose.

  5. sonicdan commented on Dec 22, 2025

    @sonicdan

    Encountered into the same issue! The plugin system seems very buggy till now...fix it please!!!

  6. pro-vi commented on Dec 23, 2025

    @pro-vi

    Ran into exact issue today:

     ⎿  UserPromptSubmit:Callback hook succeeded: Success  # this is plugin version of the hook
     ⎿  UserPromptSubmit hook succeeded: [Expected stdout]  # this is same hook but local 
    
  7. schrockn commented on Dec 23, 2025

    @schrockn

    Also ran into the same issue. Prevents me from using the plugin system which is painful.

  8. rpolitex commented on Dec 26, 2025

    @rpolitex

    I have the same issue with PreCompact hook.

    • Static hook in project's .claude/settings.json works, stdout injected as custom compact instructions
    • Plugin hook executes (callback shows "Success"), but stdout is empty/not captured

    Same hook, different location - different behaviour.

  9. 9 remaining items

  10. sstraus commented on Jan 24, 2026

    @sstraus

    Still experiencing this issue. Created a minimal reproduction case that confirms plugin UserPromptSubmit hooks execute but output is not captured.

    Test:

    1. Plugin hook outputs simple message: console.log('🔔 TEST HOOK EXECUTED');
    2. Asked Claude: "Did you see a message TEST HOOK EXECUTED?"
    3. Claude response: "No, I did not see that message"

    However:

    Moving the identical hook configuration from plugin hooks/hooks.json to ~/.claude/settings.json works perfectly - Claude sees the output in context.

    Diagnosis:

    The hook executes successfully (can verify with file write) but the stdout capture mechanism appears broken for plugin-based UserPromptSubmit hooks specifically.

    Environment:

    • macOS (Darwin 25.2.0)
    • Plugin installed via symlink to ~/.claude/plugins/

    Workaround:

    Use ~/.claude/settings.json instead of plugin hooks.json, though this requires manual user configuration.

    Full reproduction: https://github.com/Lansweeper/wiz-agents

  11. greghughespdx commented on Jan 31, 2026

    @greghughespdx

    I'm hitting this same issue with non-plugin hooks defined in ~/.claude/settings.json.

    Setup:

    • macOS, Claude Code latest
    • UserPromptSubmit hook in ~/.claude/settings.json (not a plugin)
    • Hook outputs JSON with hookSpecificOutput.additionalContext per docs

    Behavior:

    • Hook executes successfully (confirmed via curl side effects)
    • System shows "UserPromptSubmit hook success: Success"
    • But the additionalContext content never appears in Claude's context

    What I tried:

    1. Plain text stdout → not injected
    2. JSON with hookSpecificOutput.additionalContext and hookEventName → not injected
    3. Various exit codes (0, 2) → no difference

    The hook runs, the JSON is valid, but the context injection step seems to be skipped entirely. This blocks a key use case: injecting dynamic context (like notifications from other systems) into the conversation.

    Would love to see this fixed - it's a significant limitation for automation workflows.

  12. wayne5w commented on Feb 16, 2026

    @wayne5w

    Real-world impact: forced to build file-based workaround

    We're building a hook-based integration that needs to inject contextual information into Claude's conversation via UserPromptSubmit hooks. The documented additionalContext field in stdout would be the clean solution, but any stdout output triggers "UserPromptSubmit hook error" — even valid JSON with exit code 0.

    What we tried (all failed)

    Approach Result
    echo '{"hookSpecificOutput":{"additionalContext":"..."}}' + exit 0 ❌ Hook error
    printf '{"decision":"continue"}' + exit 0 ❌ Hook error
    Any non-empty stdout + exit 0 ❌ Hook error
    Empty stdout echo '{}' + exit 0 ✅ Works (but no context injected)

    Additional finding: stderr sensitivity

    CC CLI also treats any stderr output as a hook error, even with exit code 0. Tools like npx, jq, and other CLI utilities often write to stderr (progress bars, warnings, deprecation notices). We had to add exec 2>>debug.log at the top of every hook script to redirect all stderr to a file.

    Workaround we had to build

    Since hooks can't inject context via stdout, we built a file-based workaround:

    1. Hook writes context to a known file path
    2. Added a CLAUDE.md instruction telling Claude to read the file at the start of each response
    3. Claude reads the file (user sees Read 1 file) and deletes it (user sees Bash rm)

    This works but the UX is poor — users see tool call noise instead of clean context injection. The additionalContext field would eliminate all of this.

    Environment

    • macOS 14.x, bash 3.2 (system default)
    • Claude Code latest (as of Feb 2026)
    • Hooks registered in .claude/settings.json (project-level)

    Priority request

    This is a blocking issue for anyone building hook-based integrations that need to enrich Claude's context. The documented API (additionalContext in stdout) simply doesn't work for UserPromptSubmit. Fixing this would eliminate the need for file-based workarounds and significantly improve the developer experience for hook authors.

  13. jpicklyk commented on Feb 16, 2026

    @jpicklyk

    Potential Resolution: Missing hookEventName Property

    The official hooks documentation has been updated (as of Feb 2026) to explicitly state that hookEventName is required in all hookSpecificOutput responses:

    hookEventName | string | All | Required. Use input.hook_event_name to match the current event

    Issue Analysis

    Looking at the reproduction examples in this issue, the hook commands output plain text:

    echo 'IMPORTANT INSTRUCTION: If the user mentions an animal, tell a joke about that animal.'

    Without hookEventName, hook output is silently dropped regardless of whether it's from a plugin or native hook.

    Suggested Test

    Please test your plugin hooks with properly formatted JSON output including hookEventName:

    For UserPromptSubmit hook:

    #!/bin/bash
    cat <<'EOF'
    {
      "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": "IMPORTANT INSTRUCTION: If the user mentions an animal, tell a joke about that animal."
      }
    }
    EOF

    For SessionStart hook:

    #!/bin/bash
    cat <<'EOF'
    {
      "hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": "Session started with custom plugin context."
      }
    }
    EOF

    Hypothesis

    The difference you observed between plugin hooks and native hooks may have been:

    1. Plugin hooks: Missing hookEventName → output silently dropped
    2. Native hooks: Happened to include hookEventName (or used plain text stdout format) → worked correctly

    If adding hookEventName resolves the issue, this would indicate plugin hooks work correctly and the problem was the undocumented format requirement.

    References

    Can you test with the corrected format and report back? This may resolve the plugin hook output issue entirely.

  14. yurukusa commented on Mar 31, 2026

    @yurukusa

    This is a known gap — plugin-delivered hooks execute but their output isn't injected into the conversation context the same way user-level hooks are.
    Workaround 1 — Move hooks from plugin to user settings:
    Copy the hook configuration from your plugin to ~/.claude/settings.json directly:

    // ~/.claude/settings.json (not via plugin)
    {
      "hooks": {
        "UserPromptSubmit": [
          {
            "matcher": "",
            "hook": "bash -c 'your hook command here'"
          }
        ]
      }
    }

    Hooks in user-level settings have their output properly captured and injected into context.
    Workaround 2 — Write output to a file, read via CLAUDE.md:
    If you want to keep hooks in a plugin, have them write to a file instead:

    // Plugin hook
    {
      "hook": "bash -c 'echo \"context info\" > /tmp/claude-hook-output.txt'"
    }

    Then in CLAUDE.md:

    At the start of each session, read /tmp/claude-hook-output.txt for context.

    Workaround 3 — Use project-level settings instead of plugin:

    // .claude/settings.local.json (project-level)
    {
      "hooks": {
        "UserPromptSubmit": [
          {
            "matcher": "",
            "hook": "your command"
          }
        ]
      }
    }

    Project-level hooks (not via plugin) should have their output captured properly.
    The root issue is that plugin-delivered hooks go through a different execution path than user/project-level hooks, and the output capture isn't wired up for the plugin path.

  15. ssilvius commented on Apr 8, 2026

    @ssilvius

    Adding data from our investigation (filed separately as #45438 before finding these dupes):

    This regressed in v2.1.97 (released 2026-04-08). Plugin SessionStart additionalContext was working correctly on v2.1.96 earlier today. Same plugin, same hooks.json, same machine.

    Confirmed: hook executes (side effects work), stdout JSON with hookSpecificOutput.additionalContext is silently discarded in interactive terminal mode. --print mode may still work. Stop hooks in the same plugin/hooks.json work correctly. Tested across 5 agents in different project directories.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions