Skip to content

fork subagent hardening: unbounded turn count + permission-gated tool calls silently auto-denied #5734

Description

@qqqys

Two related robustness gaps in the detached fork subagent (subagent_type: "fork" / /fork). Both stem from the fork running fire-and-forget in the background with no inline UI.

1. Fork has no turn cap (runaway token burn)

createForkSubagent launches the fork's reasoning loop with an empty RunConfig:

packages/core/src/tools/agent/agent.tsAgentHeadless.create(FORK_AGENT.name, agentConfig, promptConfig, {}, {} as RunConfig, toolConfig, eventEmitter).

{} as RunConfig leaves max_turns unset, and AgentCore only enforces a cap when options.maxTurns is truthy (agent-core.ts). A fork is fire-and-forget — nobody awaits it — so a loop that fails to converge burns tokens silently with no upper bound.

Fix: pass a default turn cap (max_turns: 200, matching claude-code's fork). Define FORK_DEFAULT_MAX_TURNS = 200 in fork-subagent.ts and pass { max_turns: FORK_DEFAULT_MAX_TURNS }.

2. Permission-gated tool calls are silently auto-denied (should bubble)

FORK_AGENT sets approvalMode: 'default' (fork-subagent.ts:36). In the background launch path, shouldBubble = subagentConfig.approvalMode === BUBBLE_APPROVAL_MODE && isInteractive() (agent.ts:2226) — false for a fork — so bgConfig.getShouldAvoidPermissionPrompts = () => true (agent.ts:2234). With no inline UI, a tool call needing confirmation is auto-denied; since the fork is fire-and-forget, no prompt ever reaches the user.

A fork therefore cannot perform any permission-gated action — including the <fork-boilerplate>-mandated "commit your changes before reporting" — whenever the parent is in default or plan mode (the common interactive case). Forks from yolo / auto-edit / auto parents auto-approve and are unaffected.

The bubble → parent-UI bridge (shouldBubble ? cleanupApprovalBridge…, registry.bridgeApprovalEvents, TOOL_WAITING_APPROVALagent.ts:2449) already exists for exactly this interactive-but-detached case. claude-code's fork agent uses permissionMode: 'bubble'.

Fix: set FORK_AGENT.approvalMode to 'bubble' (BUBBLE_APPROVAL_MODE). bubble resolves to Default run behavior (tool calls require confirmation) and only flips deny → surface for interactive sessions; non-interactive forks are gated off (isForkSubagentEnabled = config.isInteractive()), so there is no headless regression.

Repro (problem 2)

  1. Interactive session, approval mode default.
  2. /fork "make a one-line edit to README and commit it".
  3. The fork's edit/commit (or any non-allowlisted shell) is auto-denied; the fork reports it couldn't finish, with no permission prompt shown.

Fork subagent introduced in #5155. PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions