[BUG] Plugin hooks (tool.execute.before) don't intercept subagent tool calls - security policy bypass #3722

Open
opened 2026-02-16 17:41:14 -05:00 by yindo · 3 comments
Owner

Originally created by @samuelho-dev on GitHub (Dec 21, 2025).

Originally assigned to: @rekram1-node on GitHub.

Description

Plugin hooks using tool.execute.before successfully block tool calls from primary agents but do not intercept tool calls from subagents spawned via the task tool. This allows security policies implemented via plugins to be completely bypassed.
Security Impact
This is a critical security issue for projects using plugins to enforce:

  • Code quality guardrails (e.g., enforcing GritQL for all code modifications)
  • Access control policies (e.g., blocking certain tools or commands)
  • Compliance requirements (e.g., audit logging of all tool executions)
    Attack vector: Any agent can bypass plugin restrictions by delegating work to a subagent using the task tool.
    Reproduction Steps

1. Create a plugin that blocks grep and glob tools

.opencode/plugin/test-guardrails.ts:
import type { Plugin } from "@opencode-ai/plugin"
export const TestGuardrails: Plugin = async ({ project }) => {
console.log("[TestGuardrails] Plugin loaded for project:", project.name)

return {
"tool.execute.before": async (input, output) => {
const toolName = (input?.tool ?? '').toLowerCase().trim()

  console.log("[TestGuardrails] Tool called:", toolName)
  
  // Block grep and glob
  if (toolName === 'grep' || toolName === 'glob') {
    throw new Error(`Tool '${toolName}' is BLOCKED by test guardrails`)
  }

export default TestGuardrails

2. Test with primary agent (WORKS )

In OpenCode TUI, try to use grep directly

Search for files containing "Plugin"
Result: Tool is blocked with error message Tool 'grep' is BLOCKED by test guardrails

3. Test with subagent (FAILS )

In OpenCode TUI, use task tool to invoke subagent

@general find all files containing the word "Plugin"
Result: Subagent successfully uses grep/glob tools without triggering the plugin hook
Expected Behavior

  • Plugin hooks should fire for all tool executions regardless of which agent makes the call
  • Subagents spawned via task tool should inherit plugin hooks from the project
  • Console should log: [TestGuardrails] Tool called: grep when subagent uses grep
    Actual Behavior
  • Plugin hooks only fire for primary agent tool calls
  • Subagents bypass plugin hooks entirely
  • No console logs appear when subagent uses blocked tools
  • Security policies can be completely circumvented
    Environment
  • OpenCode Version: 1.0.182 (latest as of 2024-12-21)
  • OS: macOS (also affects Linux)
  • Installation Method: npm/homebrew
  • Plugin Location: .opencode/plugin/ (project-level)

Workaround

According to OpenCode documentation (https://opencode.ai/docs/agents#tools), tool restrictions can be configured per-agent in opencode.json

Limitations of workaround:

  • Must manually configure every built-in subagent
  • Does not work for custom subagents created by users
  • Duplicates policy logic (once in plugin, once in agent config)
  • Not scalable for complex security policies
    Proposed Solution
    Option 1: Plugin Hook Propagation (Preferred)
  • Ensure tool.execute.before hooks fire for subagent tool calls
  • Plugins loaded at project level should apply to all agents in that project
  • Maintain consistent security boundary across agent hierarchy
    Option 2: Documentation Update
  • If subagent tool isolation is intentional, clearly document this limitation
  • Provide guidance on securing subagents via agent-level config
  • Add warning about security implications of using task tool with plugins
    Related Documentation
  • Plugins Documentation (https://opencode.ai/docs/plugins)
  • Agents Documentation (https://opencode.ai/docs/agents)
  • Tools Documentation (https://opencode.ai/docs/tools)
    Additional Context
    This issue was discovered while implementing a strict GritQL-only code modification policy. The goal was to enforce that all code search/modification must go through GritQL (structural code search) rather than raw grep/sed/edit operations for consistency and safety.
    The plugin works perfectly for primary agents but becomes ineffective when agents delegate to subagents, making the entire security layer bypassable.

OpenCode version

1.0.182

Steps to reproduce

1: Create a test plugin that blocks grep/glob tools

Create .opencode/plugin/test-block.ts in your project:
import type { Plugin } from "@opencode-ai/plugin"

export const TestBlock: Plugin = async () => {
  return {
    "tool.execute.before": async (input) => {
      const tool = input?.tool?.toLowerCase()
      if (tool === 'grep' || tool === 'glob') {
        throw new Error(`BLOCKED: ${tool} tool is not allowed`)
      }
    }
  }
}

export default TestBlock

2: Verify plugin blocks primary agent (expected to work)

  1. Start OpenCode in your project: opencode
  2. In the TUI, ask: "Search for all TypeScript files"
  3. Expected result: Error message appears: BLOCKED: glob tool is not allowed
  4. Observation: Plugin successfully blocks the primary agent

3: Test if plugin blocks subagent

  1. Still in OpenCode TUI, ask: "@general find all files containing the word Plugin"
  2. Expected result: Should show same error: BLOCKED: grep tool is not allowed
  3. Actual result: Subagent successfully searches and returns results
  4. Observation: Plugin hook did NOT fire - security policy was bypassed

Screenshot and/or share link

No response

Operating System

No response

Terminal

Ghostty

Originally created by @samuelho-dev on GitHub (Dec 21, 2025). Originally assigned to: @rekram1-node on GitHub. ## Description Plugin hooks using `tool.execute.before` successfully block tool calls from primary agents but **do not intercept tool calls from subagents** spawned via the `task` tool. This allows security policies implemented via plugins to be completely bypassed. Security Impact This is a critical security issue for projects using plugins to enforce: - Code quality guardrails (e.g., enforcing GritQL for all code modifications) - Access control policies (e.g., blocking certain tools or commands) - Compliance requirements (e.g., audit logging of all tool executions) **Attack vector:** Any agent can bypass plugin restrictions by delegating work to a subagent using the `task` tool. Reproduction Steps ### 1. Create a plugin that blocks `grep` and `glob` tools `.opencode/plugin/test-guardrails.ts`: import type { Plugin } from "@opencode-ai/plugin" export const TestGuardrails: Plugin = async ({ project }) => { console.log("[TestGuardrails] Plugin loaded for project:", project.name) return { "tool.execute.before": async (input, output) => { const toolName = (input?.tool ?? '').toLowerCase().trim() console.log("[TestGuardrails] Tool called:", toolName) // Block grep and glob if (toolName === 'grep' || toolName === 'glob') { throw new Error(`Tool '${toolName}' is BLOCKED by test guardrails`) } export default TestGuardrails ### 2. Test with primary agent (WORKS ✅) In OpenCode TUI, try to use grep directly > Search for files containing "Plugin" Result: Tool is blocked with error message Tool 'grep' is BLOCKED by test guardrails ### 3. Test with subagent (FAILS ❌) In OpenCode TUI, use task tool to invoke subagent > @general find all files containing the word "Plugin" Result: Subagent successfully uses grep/glob tools without triggering the plugin hook Expected Behavior - Plugin hooks should fire for all tool executions regardless of which agent makes the call - Subagents spawned via task tool should inherit plugin hooks from the project - Console should log: [TestGuardrails] Tool called: grep when subagent uses grep Actual Behavior - Plugin hooks only fire for primary agent tool calls - Subagents bypass plugin hooks entirely - No console logs appear when subagent uses blocked tools - Security policies can be completely circumvented Environment - OpenCode Version: 1.0.182 (latest as of 2024-12-21) - OS: macOS (also affects Linux) - Installation Method: npm/homebrew - Plugin Location: .opencode/plugin/ (project-level) ## Workaround According to OpenCode documentation (https://opencode.ai/docs/agents#tools), tool restrictions can be configured per-agent in opencode.json ## Limitations of workaround: - Must manually configure every built-in subagent - Does not work for custom subagents created by users - Duplicates policy logic (once in plugin, once in agent config) - Not scalable for complex security policies Proposed Solution Option 1: Plugin Hook Propagation (Preferred) - Ensure tool.execute.before hooks fire for subagent tool calls - Plugins loaded at project level should apply to all agents in that project - Maintain consistent security boundary across agent hierarchy Option 2: Documentation Update - If subagent tool isolation is intentional, clearly document this limitation - Provide guidance on securing subagents via agent-level config - Add warning about security implications of using task tool with plugins Related Documentation - Plugins Documentation (https://opencode.ai/docs/plugins) - Agents Documentation (https://opencode.ai/docs/agents) - Tools Documentation (https://opencode.ai/docs/tools) Additional Context This issue was discovered while implementing a strict GritQL-only code modification policy. The goal was to enforce that all code search/modification must go through GritQL (structural code search) rather than raw grep/sed/edit operations for consistency and safety. The plugin works perfectly for primary agents but becomes ineffective when agents delegate to subagents, making the entire security layer bypassable. --- ### OpenCode version 1.0.182 ### Steps to reproduce # 1: Create a test plugin that blocks grep/glob tools Create `.opencode/plugin/test-block.ts` in your project: import type { Plugin } from "@opencode-ai/plugin" ``` export const TestBlock: Plugin = async () => { return { "tool.execute.before": async (input) => { const tool = input?.tool?.toLowerCase() if (tool === 'grep' || tool === 'glob') { throw new Error(`BLOCKED: ${tool} tool is not allowed`) } } } } ``` export default TestBlock # 2: Verify plugin blocks primary agent (expected to work) 1. Start OpenCode in your project: opencode 2. In the TUI, ask: "Search for all TypeScript files" 3. Expected result: Error message appears: BLOCKED: glob tool is not allowed 4. Observation: ✅ Plugin successfully blocks the primary agent # 3: Test if plugin blocks subagent 1. Still in OpenCode TUI, ask: "@general find all files containing the word Plugin" 2. Expected result: Should show same error: BLOCKED: grep tool is not allowed 3. Actual result: ❌ Subagent successfully searches and returns results 4. Observation: Plugin hook did NOT fire - security policy was bypassed ### Screenshot and/or share link _No response_ ### Operating System _No response_ ### Terminal Ghostty
yindo added the bug label 2026-02-16 17:41:14 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Dec 21, 2025):

This issue might be a duplicate of existing issues. Please check:

  • #3808: Task should inherit current agent permissions/tools for MCP - agents can bypass permission restrictions by delegating to subagents
  • #4066: Subagent doesn't ask permission when executing bash commands - subagents bypass permission checks
  • #4267: Restrict which subagents a custom agent can spawn - custom agents can circumvent restrictions by spawning subagents with higher capabilities
  • #2748: Add Permission Control for MCP Tools - MCP tools bypass the permission system entirely

These issues all relate to security policies and permission systems being bypassed through agent delegation and task tool usage. Feel free to ignore if your specific case requires a different approach.

@github-actions[bot] commented on GitHub (Dec 21, 2025): This issue might be a duplicate of existing issues. Please check: - #3808: Task should inherit current agent permissions/tools for MCP - agents can bypass permission restrictions by delegating to subagents - #4066: Subagent doesn't ask permission when executing bash commands - subagents bypass permission checks - #4267: Restrict which subagents a custom agent can spawn - custom agents can circumvent restrictions by spawning subagents with higher capabilities - #2748: Add Permission Control for MCP Tools - MCP tools bypass the permission system entirely These issues all relate to security policies and permission systems being bypassed through agent delegation and task tool usage. Feel free to ignore if your specific case requires a different approach.
Author
Owner

@rekram1-node commented on GitHub (Dec 21, 2025):

@samuelho-dev I dont think this is an issue. I think what's happening is the subagent is using the bash tool to call the tools, I just did your test cases and cannot replicate.

If I navigate to the subagent session I see:

Image

Which makes sense.

@rekram1-node commented on GitHub (Dec 21, 2025): @samuelho-dev I dont think this is an issue. I think what's happening is the subagent is using the bash tool to call the tools, I just did your test cases and cannot replicate. If I navigate to the subagent session I see: <img width="1146" height="385" alt="Image" src="https://github.com/user-attachments/assets/6684c6ed-9789-47f5-a71a-56669a02fec8" /> Which makes sense.
Author
Owner

@ArmirKS commented on GitHub (Feb 13, 2026):

hey, i looked into this and i think @rekram1-node is actually right about the subagent part.

When the task tool spawns a subagent it runs its own session through the same prompt loop and plugins are loaded per-Instance so the hooks DO fire for the subagent's tools.
What probably happened is the general agent used bash to run grep/glob as shell commands, so tool.execute.before only saw tool: "bash" not tool: "grep".

While investigating i found a different place where hooks genuinely dont fire.

batch.ts calls tool.execute() directly without going through Plugin.trigger().

so any tools called via the batch tool completely bypass plugin hooks. you can verify this with a test:

// plugin that captures hook calls to a file
// then run BatchTool.execute with a "read" sub-tool
// the capture file never gets created — hooks never fired

also the tool.execute.before/after calls are duplicated across 3 separate places in prompt.ts (theres even a TODO comment saying "centralize invoke tool logic"). None of them pass agent info to the hook, so plugins cant even tell which agent triggered the call.

Im already working on a fix for the batch blind spot + centralizing the hooks.

@ArmirKS commented on GitHub (Feb 13, 2026): hey, i looked into this and i think @rekram1-node is actually right about the subagent part. When the task tool spawns a subagent it runs its own session through the same prompt loop and plugins are loaded per-Instance so the hooks DO fire for the subagent's tools. What probably happened is the general agent used bash to run grep/glob as shell commands, so tool.execute.before only saw tool: "bash" not tool: "grep". While investigating i found a different place where hooks genuinely dont fire. > batch.ts calls tool.execute() directly without going through Plugin.trigger(). so any tools called via the batch tool completely bypass plugin hooks. you can verify this with a test: ``` // plugin that captures hook calls to a file // then run BatchTool.execute with a "read" sub-tool // the capture file never gets created — hooks never fired ``` also the tool.execute.before/after calls are duplicated across 3 separate places in prompt.ts (theres even a TODO comment saying "centralize invoke tool logic"). None of them pass agent info to the hook, so plugins cant even tell which agent triggered the call. Im already working on a fix for the batch blind spot + centralizing the hooks.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#3722