claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

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

Open jwaldrip opened this issue 1 month ago • 9 comments

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] 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:

  • #10225 - UserPromptSubmit hooks never execute (closed as duplicate, but about execution not output)
  • #11509 - SessionStart hooks never execute for local file-based plugins (about execution not output)
  • #9708 - Notification hooks don't execute (different hook type, about execution not output)

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.

jwaldrip avatar Nov 22 '25 19:11 jwaldrip

I'm also getting this

informal-stripes-condo avatar Nov 26 '25 17:11 informal-stripes-condo

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.

crsmithdev avatar Nov 30 '25 21:11 crsmithdev

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.

RevPalSFDC avatar Dec 05 '25 00:12 RevPalSFDC

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.

fmunteanu avatar Dec 18 '25 18:12 fmunteanu

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

sonicdan avatar Dec 22 '25 12:12 sonicdan

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 

pro-vi avatar Dec 23 '25 03:12 pro-vi