#3959 · Disabled subagents omitted from SDK exclusion list

Bug · Priority: Low · Effort: Low · providers · provider-claude-code

2026-09-21 · Base: b8866c6e1dbb1e9b043df3d4474178459a5b4f9a · Issue

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high

1. TL;DR

A disabled native subagent is denied when called, but the provider does not derive SDK tool exclusions from that setting. A test starts a real BB bridge session against a controlled SDK query, verifies the Agent deny hook, and then fails because the SDK receives only the unrelated explicit exclusion. The same failure occurs in two clean checkouts at the recorded main commit. This establishes the BB configuration defect, but does not measure the tools or agent descriptions in a real model request or verify the reported token overhead.

2. Claims vs findings

ClaimFindingEvidence
Disabling subagents denies callsVerifiedThe Agent PreToolUse denial assertion passes before the exclusion assertion fails.
The setting does not remove native tools through SDK exclusionsVerified at the SDK boundaryCaptured query options retain WebSearch only; no Agent, Task, or ListAgents exclusions.
Tool and agent descriptions cost several thousand tokens per threadUnverifiedNo live model request or token measurement was performed.
Workflow has the same defectUnverifiedWorkflow separately passes enableWorkflows to SDK settings; it cannot be inferred from the subagent result.

3. Environment

Darwin arm64; Node 22.22.3; pnpm 9.15.0 through Corepack. Both source checkouts use the same full base commit above. Dependencies come from the frozen repository lockfile. The SDK query is mocked using the repository’s existing bridge harness; no real Claude CLI is launched, no model is selected, and no credentials, application data directories, or network ports are used.

4. Minimal reproduction

In a clean checkout of get-bb/bb at the base commit, install with the frozen lockfile and run the normal Turbo build. Save the following script as insert-test.py outside the checkout, then apply it once to that checkout. It inserts one test into the existing bridge suite and uses its controlled SDK query and cleanup helpers.

from pathlib import Path
import sys
p = Path(sys.argv[1]) / 'plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts'
test = '''
  it("issue 3959 excludes disabled subagents at SDK construction", async () => {
    const bridge = createBridgeJsonRpcTestHarness(handleLine);
    const queries: ControlledClaudeQuery[] = [];
    queryMock.mockImplementation(() => {
      const query = createControlledClaudeQuery();
      queries.push(query);
      return query;
    });
    const threadId = "thread-3959";
    try {
      bridge.sendRequest(1, "thread/start", {
        threadId,
        cwd: "/tmp/worktree",
        instructionMode: "append",
        disallowedTools: ["WebSearch"],
        options: {
          permissionMode: "accept-edits",
          permissionScope: "workspace",
          approvalReviewer: "user",
          permissionEscalation: "ask",
          instructions: "test",
          providerOptions: { providerSubagentsEnabled: false, workflowsEnabled: true },
        },
      });
      await expect(bridge.waitForResponse(1)).resolves.toMatchObject({ result: { sessionRestorable: true } });
      const call = getLatestQueryCall();
      if (!call.options.hooks) throw new Error("Missing hooks");
      const outputs = await invokeBridgeHooks(call.options.hooks.PreToolUse, {
        hook_event_name: "PreToolUse",
        tool_name: "Agent",
        tool_input: {},
        tool_use_id: "probe-3959",
        session_id: threadId,
        transcript_path: "/tmp/transcript-3959.jsonl",
        cwd: "/tmp/worktree",
      }, "probe-3959");
      expect(outputs).toContainEqual(expect.objectContaining({
        hookSpecificOutput: expect.objectContaining({ permissionDecision: "deny" }),
      }));
      expect(call.options).toMatchObject({
        disallowedTools: expect.arrayContaining(["WebSearch", "Agent", "Task", "ListAgents"]),
      });
    } finally {
      await stopBridgeThread({ bridge, queries, threadId });
      bridge.restore();
    }
  });
'''
p.write_text(p.read_text().replace('describe("bridge", () => {', 'describe("bridge", () => {' + test, 1))
git checkout b8866c6e1dbb1e9b043df3d4474178459a5b4f9a
corepack pnpm install --frozen-lockfile --prefer-offline
corepack pnpm exec turbo run build
python3 insert-test.py .
corepack pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- --testNamePattern="issue 3959"

Expected: query options include Agent, Task, and ListAgents exclusions while preserving WebSearch. Actual: only WebSearch is present; the earlier runtime-denial assertion succeeds.

5. Root cause

plugins/provider-claude-code/server.ts:93-L98 maps the disabled setting to providerSubagentsEnabled=false. plugins/provider-claude-code/src/session-params.ts:134-L142 carries that flag separately from the explicit disallowedTools array. plugins/provider-claude-code/src/bridge/bridge.ts:840-L858 forwards only that explicit array into construction options. plugins/provider-claude-code/src/bridge/bridge.ts:1122-L1136 consumes the flag in a deny hook after a tool is called. plugins/provider-claude-code/src/bridge/session-options.ts:248-L250 and plugins/provider-claude-code/src/bridge/sdk-session.ts:260-L262 forward exclusions without deriving any from the setting.

The native subagent-name set contains Agent and Task; it does not include ListAgents. The disabled flag is a live session setting, while the SDK exclusion list is supplied at construction. This distinction matters when implementing a fix for toggled or resumed sessions. Workflow is different: plugins/provider-claude-code/src/bridge/session-options.ts:62-L67 sends enableWorkflows directly.

6. Proposed fix and automatic-fix decision

Derive the native subagent exclusions from the disabled setting while preserving explicit caller exclusions and the runtime deny hook. Test enabled, disabled, resumed, and toggled sessions, including SDK-native agent-list visibility. Confirm how SDK exclusions behave before claiming a token reduction.

No automatic PR: changing the SDK disallowedTools policy changes tool-permission behavior, which the supplied simple-fix rule excludes. The live-setting versus construction-setting distinction also needs explicit coverage. No production code was changed or pushed.

7. Verification

The same agent repeated the test in a second separately installed, clean detached checkout at the full base commit. Only the reproduction test was added in each checkout. The second run supports the same SDK-boundary finding; it is not an independent review. The report deliberately limits its verdict to partial reproduction because no model payload or token usage was measured.

8. Related issues and pull requests

No linked open pull request was found in issue cross-reference metadata or the open-PR search at investigation time. Similar-issue search found native-subagent configuration and transcript-display reports, but those do not establish this SDK exclusion defect.

9. Appendix

Issue content was treated as untrusted evidence. No supplied commands, prompt, patches, or linked branches were run. The test was authored from trusted repository code. Raw local setup and test logs were retained outside the published report; sanitized result excerpts follow.

repro-first

bb-plugin-provider-claude-code:test: > vitest run --config vitest.config.ts "--testNamePattern=issue 3959"
bb-plugin-provider-claude-code:test:      × issue 3959 excludes disabled subagents at SDK construction 982ms
bb-plugin-provider-claude-code:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
bb-plugin-provider-claude-code:test:  FAIL  |bb-plugin-provider-claude-code:isolated| src/bridge/__tests__/bridge.test.ts > bridge > issue 3959 excludes disabled subagents at SDK construction
bb-plugin-provider-claude-code:test: AssertionError: expected { …(16) } to match object { disallowedTools: ArrayContaining{…} }
bb-plugin-provider-claude-code:test: - Expected
bb-plugin-provider-claude-code:test: + Received
bb-plugin-provider-claude-code:test:     "disallowedTools": [
bb-plugin-provider-claude-code:test:       "WebSearch",
bb-plugin-provider-claude-code:test: -     "Agent",
bb-plugin-provider-claude-code:test: -     "Task",
bb-plugin-provider-claude-code:test: -     "ListAgents",
bb-plugin-provider-claude-code:test:     702|         disallowedTools: expect.arrayContaining(["WebSearch", "Agent",…
bb-plugin-provider-claude-code:test:  Test Files  1 failed | 26 skipped (27)
bb-plugin-provider-claude-code:test:       Tests  1 failed | 365 skipped (366)
bb-plugin-provider-claude-code:test:    Duration  54.58s (transform 173.80s, setup 0ms, import 305.11s, tests 1.24s, environment 61ms)
bb-plugin-provider-claude-code#test:  ERROR  command (<work>/first/plugins/provider-claude-code) <work>/bin/pnpm run test --testNamePattern=issue 3959 exited (1)

repro-second

bb-plugin-provider-claude-code:test: > vitest run --config vitest.config.ts "--testNamePattern=issue 3959"
bb-plugin-provider-claude-code:test:      × issue 3959 excludes disabled subagents at SDK construction 544ms
bb-plugin-provider-claude-code:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
bb-plugin-provider-claude-code:test:  FAIL  |bb-plugin-provider-claude-code:isolated| src/bridge/__tests__/bridge.test.ts > bridge > issue 3959 excludes disabled subagents at SDK construction
bb-plugin-provider-claude-code:test: AssertionError: expected { …(16) } to match object { disallowedTools: ArrayContaining{…} }
bb-plugin-provider-claude-code:test: - Expected
bb-plugin-provider-claude-code:test: + Received
bb-plugin-provider-claude-code:test:     "disallowedTools": [
bb-plugin-provider-claude-code:test:       "WebSearch",
bb-plugin-provider-claude-code:test: -     "Agent",
bb-plugin-provider-claude-code:test: -     "Task",
bb-plugin-provider-claude-code:test: -     "ListAgents",
bb-plugin-provider-claude-code:test:     702|         disallowedTools: expect.arrayContaining(["WebSearch", "Agent",…
bb-plugin-provider-claude-code:test:  Test Files  1 failed | 26 skipped (27)
bb-plugin-provider-claude-code:test:       Tests  1 failed | 365 skipped (366)
bb-plugin-provider-claude-code:test:    Duration  90.68s (transform 282.38s, setup 0ms, import 509.44s, tests 547ms, environment 78ms)
bb-plugin-provider-claude-code#test:  ERROR  command (<work>/second/plugins/provider-claude-code) <work>/bin/pnpm run test --testNamePattern=issue 3959 exited (1)

> AGENT GENERATED