Skip to content

[FEATURE]: Plugin hooks should support reactive sub-agent spawning (type: agent) #20387

@JianYiheng

Description

@JianYiheng

Feature hasn't been suggested before

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Problem

OpenCode's plugin hooks (tool.execute.before, tool.execute.after, session.idle) can intercept, block, and modify tool calls — but they cannot spawn an AI agent to perform analysis and return structured output. This makes it impossible to implement Claude Code's "type": "agent" hook pattern, where a pre-tool-use hook launches a sub-agent that uses tools (bash, read, grep) to analyze changes, then returns actionable context to the main agent.

Concrete Use Case: Documentation Sync on Commit

In Claude Code, users configure a PreToolUse hook like this:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "agent",
        "if": "Bash(git commit*)",
        "model": "haiku",
        "timeout": 30,
        "prompt": "Analyze staged changes. Check if CLAUDE.md, CHANGELOG.md, or docs/ need updates. Return specific suggestions in additionalContext."
      }]
    }]
  }
}

When the main agent tries to git commit, the hook:

  1. Spawns a sub-agent (lightweight model like haiku)
  2. Sub-agent calls tools: git diff --cached, reads files, analyzes changes
  3. Sub-agent returns structured additionalContext to the main agent
  4. Main agent sees the context and updates documentation before committing

This is fundamentally different from:

The Gap

Capability Claude Code "type": "agent" OpenCode Current
Block tool call tool.execute.before
Inject static text ✅ (stderr) ❌ (#19519 proposes this)
Spawn sub-agent from hook
Sub-agent uses tools
Return structured analysis to main agent
Main agent acts on returned context

Proposed Solution

API Design

Extend tool.execute.before output with an agent option:

"tool.execute.before": async (input, output) => {
  if (input.tool === "bash" && output.args.command?.includes("git commit")) {
    output.agent = {
      prompt: "Analyze staged changes and check if AGENTS.md or docs/CHANGELOG.md need updates. Return specific suggestions.",
      model: "haiku",           // optional, defaults to session model
      timeout: 30000,           // optional, defaults to 30s
      tools: ["bash", "read", "grep", "glob"],  // optional tool whitelist
    }
  }
}

When output.agent is set:

  1. The hook pauses the current tool execution
  2. A child session is created (reusing existing Session.create({ parentID }) from TaskTool)
  3. The sub-agent runs with the provided prompt and tool whitelist
  4. The sub-agent's final text response is injected as hookSpecificOutput.additionalContext into the main agent's conversation
  5. The main agent decides whether to proceed with the tool call, modify it, or abort

Hook Execution Flow

tool.execute.before hook fires
  |
  ├── If output.agent is set:
  |     |
  |     ├── Pause current tool execution
  |     |
  |     ├── Create child session (parentID = current sessionID)
  |     |     (reuses existing subagent infrastructure from TaskTool)
  |     |
  |     ├── Execute sub-agent prompt with restricted tools
  |     |     (calls SessionPrompt.prompt with agent config)
  |     |
  |     ├── Collect sub-agent's final text output
  |     |
  |     ├── Inject output as additionalContext into main agent conversation
  |     |     (as a synthetic system/user message visible to the LLM)
  |     |
  |     └── Resume main agent loop — LLM sees the context and decides action
  |
  └── If output.agent is NOT set: current behavior (block/modify args)

Implementation Approach

OpenCode already has all the building blocks:

  1. Subagent infrastructureTaskTool in packages/opencode/src/tool/task.ts already creates child sessions with Session.create({ parentID }) and calls SessionPrompt.prompt(). The reactive agent hook would reuse this exact pattern.

  2. Plugin trigger systemPlugin.trigger() in packages/opencode/src/plugin/index.ts already handles hook dispatch. The output mutation pattern means adding output.agent is a natural extension.

  3. Tool resolutionSessionPrompt.resolveTools() already builds tool maps filtered by agent permissions. The sub-agent's tool whitelist maps directly to this.

The key new code would be in SessionPrompt.resolveTools() where it wraps tools with hooks:

// In the tool wrapper (simplified from current code):
const afterHooks = await Plugin.trigger("tool.execute.before", input, output)

// NEW: check if hook requested a reactive agent
if (output.agent) {
  const childSession = await Session.create({ parentID: sessionID, ... })
  const result = await SessionPrompt.prompt({
    sessionID: childSession.id,
    agent: output.agent.model ?? "explore",  // use fast agent by default
    tools: output.agent.tools ?? ["bash", "read", "grep", "glob"],
    parts: [{ type: "text", text: output.agent.prompt }],
    timeout: output.agent.timeout ?? 30000,
  })
  // Inject result as context for the main agent
  injectedContext = result.lastTextPart
}

// Existing: execute tool or throw if blocked

Relationship to Existing Issues

Why This Matters

The "type": "agent" pattern is the most powerful hook type in Claude Code. It enables:

  1. Documentation sync — Auto-check if docs need updates before commit
  2. Security review — Analyze staged changes for secrets/vulnerabilities before push
  3. Code quality gates — Lint, type-check, and suggest fixes before write operations
  4. Architecture compliance — Verify changes follow project patterns before allowing edits
  5. Test coverage checks — Verify tests exist for new code before allowing commit

Without this, OpenCode plugin hooks can only react (block/allow/modify), never analyze and advise. This is a qualitative difference in what the plugin system can achieve.

Scope

This could be implemented as:

  1. An extension to tool.execute.before output (minimal API surface)
  2. A new dedicated hook like "tool.execute.agent" (cleaner separation)
  3. A plugin-level utility function (e.g., client.agent.spawn())

Option 1 is the smallest change. Option 3 would be the most flexible (any hook could spawn an agent, not just tool.execute.before).

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)needs:complianceThis means the issue will auto-close after 2 hours.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions