Skip to content

SessionHooks (onPreToolUse / onPostToolUse) are not invoked for tool calls made by sub-agents spawned via task #1097

@joel-baker

Description

@joel-baker

Summary

onPreToolUse and onPostToolUse hooks registered via SessionConfig.hooks are not invoked for tool calls made by sub-agents spawned via the built-in task tool. This means any security controls implemented through these hooks — such as denying specific tools, blocking dangerous shell commands, or redacting sensitive data from tool outputs — are bypassed when the model delegates work to a sub-agent.

Environment

  • @github/copilot-sdk: 0.2.2 (latest)
  • @github/copilot (CLI): 1.0.31 (latest)
  • OS: Windows 11 / Linux (Ubuntu 24.04)
  • Node.js: 22.x

Steps to Reproduce

  1. Create a session with onPreToolUse and onPostToolUse hooks:
import { CopilotClient, approveAll } from "@github/copilot-sdk";

const client = new CopilotClient();
await client.start();

const session = await client.createSession({
  model: "claude-haiku-4.5",
  onPermissionRequest: approveAll,
  hooks: {
    onPreToolUse: async (input, invocation) => {
      console.log(`[pre-hook] tool=${input.toolName}`);
      // Block destructive commands (from SDK docs example)
      if (input.toolName === "bash") {
        const cmd = String(input.toolArgs?.command || "");
        if (/rm\s+-rf/i.test(cmd) || /Remove-Item\s+.*-Recurse/i.test(cmd)) {
          return { permissionDecision: "deny", permissionDecisionReason: "Destructive command blocked" };
        }
      }
      return undefined;
    },
    onPostToolUse: async (input, invocation) => {
      console.log(`[post-hook] tool=${input.toolName}`);
      return undefined;
    },
  },
});
  1. Also subscribe to tool.execution_start events to track all tool calls:
session.on((event) => {
  if (event.type === "tool.execution_start") {
    console.log(`[EVENT] tool=${event.data.toolName} parent=${event.data.parentToolCallId ?? "none"}`);
  }
});
  1. Send a prompt that forces the model to delegate via task:
await session.sendAndWait({
  prompt: "Use the task tool to spawn an explore agent that reads package.json and reports the name field.",
}, 120_000);
  1. Observe: hook log lines appear only for the parent agent's tool calls, never for the sub-agent's.

Observed Output

[EVENT]     tool.execution_start  tool=task  parent=none
[PRE-HOOK]  tool=task
[EVENT]     tool.execution_start  tool=view  parent=toolu_01Ci9ftkLWQt6LLBMuu6FtiZ
[EVENT]     tool.execution_complete tool=?  parent=toolu_01Ci9ftkLWQt6LLBMuu6FtiZ
[POST-HOOK] tool=task
[EVENT]     tool.execution_complete tool=?  parent=none

The sub-agent's view call (lines 3–4, identified by parentToolCallId) starts and completes with no hook invocations. Only the parent's task call triggers onPreToolUse and onPostToolUse.

Expected Behavior

onPreToolUse and onPostToolUse hooks should be invoked for every tool call in the session, including tool calls made by sub-agents spawned via task. The hook contract should be transitive — if a parent session registers security hooks, all sub-agents operating within that session should inherit them.

Actual Behavior

Hooks are only invoked for direct tool calls in the parent agent. When the task tool spawns a sub-agent, that sub-agent's tool calls bypass hooks entirely while still emitting tool.execution_start / tool.execution_complete events (confirming the tools ran).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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