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

[BUG] SubagentStop hook breaks tool injection for all plugin-defined subagents

Open basher83 opened this issue 1 month ago • 2 comments

Summary

The presence of ANY SubagentStop hook in any installed plugin completely breaks tool injection for ALL plugin-defined subagents. Affected subagents output fake XML-like text instead of real tool_use blocks, making them non-functional.

Environment

  • Claude Code version: Latest (Dec 2025)
  • OS: macOS Darwin 24.6.0
  • Plugin: Local marketplace plugin with SubagentStop hook

Steps to Reproduce

  1. Create a plugin with a SubagentStop hook that returns {} for non-matching projects:
{
  "hooks": {
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-complete.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
  1. The hook script exits early for non-matching projects:
# Returns {} and exits for projects without state file
if not state_file.exists():
    print(json.dumps({}))
    return
  1. Install the plugin globally
  2. In ANY project (even unrelated ones), spawn a plugin-defined subagent
  3. Observe: The subagent outputs fake XML instead of using real tools

Expected Behavior

  • SubagentStop hooks should only affect the hook's own logic
  • Plugin agents in unrelated projects should receive tools normally
  • A hook returning {} should have no effect on agent behavior

Actual Behavior

Broken Agent Output (with SubagentStop hook present)

The agent outputs fake XML as TEXT content:

I'll analyze this task. Let me start by exploring the codebase.

<function_calls>
<invoke name="Bash">
<parameter name="command">find /path -type f -name "*.yml"</parameter>
</invoke>
</function_calls>
<result>
/path/README.md
</result>

This is hallucinated output - no actual tool execution occurs.

Working Agent Output (after removing SubagentStop hook)

The agent uses real tool_use blocks:

{
  "type": "tool_use",
  "id": "toolu_018srQxEwyv7W3eazEiTf4Eu",
  "name": "Bash",
  "input": {
    "command": "git status",
    "description": "Check git status"
  }
}

Key Observations

  1. SubagentStop is unique: No other installed plugins use SubagentStop - they all use SessionStart, Stop, PreToolUse, PostToolUse, or UserPromptSubmit

  2. Hook output is correct: The hook returns {} for non-matching projects (verified via manual execution)

  3. Mere presence is the problem: The hook doesn't need to DO anything - just being registered breaks tool injection

  4. Built-in agents unaffected: The built-in Explore agent (subagent_type=Explore) works fine - only plugin-defined agents are broken

  5. Global scope: The bug affects ALL projects, not just the plugin's target project

Workaround

Remove SubagentStop hooks from all plugins. Use alternative approaches like:

  • PostToolUse hooks on the Task tool
  • Polling state files
  • Stop hooks for session-level validation

Related Issues

  • #7881 - SubagentStop cannot identify which subagent finished
  • #2825 - Stop hook breaks Task tool (very similar - Stop hooks with empty matcher break Task subagent initialization)
  • #6024 - Parallel subagents make event log useless
  • #11544 - Hooks not loading / 0 hook matchers (debug logs show "SubagentStop with query: undefined")

Impact

This bug makes SubagentStop hooks unusable for any plugin that needs per-agent lifecycle management. The hook type is documented but appears to have a severe implementation bug that prevents tool injection for plugin-defined agents.

basher83 avatar Dec 14 '25 08:12 basher83