← reports

#3034 · Claude sandbox omits user writable paths when bb adds roots

Bug Medium Effort: Low providers provider-claude-code open on GitHub 2026-09-03 · base c4991dae4

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

In workspace-scoped auto and acceptEdits sessions, bb gives the Claude Agent SDK an explicit sandbox object. When bb has additional writable roots, that object contains only bb's roots and never reads the user's Claude settings. A focused test with a synthetic home directory shows the user's configured cache path disappears while bb's thread-storage path remains. Because the omission happens while bb assembles the SDK options, it is deterministic and does not require an authenticated provider turn to reproduce.

2. Claims vs findings

ClaimStatusEvidence
bb's workspace sandbox replaces the effective user writable-path list with its own additional roots.VerifiedBoth clean runs received a one-element list containing only the synthetic bb root; the synthetic user cache path was absent.
The Claude session still asks the SDK to load user, project, and local settings.Verifiedsdk-session.ts sets all three setting sources while independently forwarding bb's sandbox object.
The affected configuration is limited to workspace-scoped auto and acceptEdits modes.VerifiedusesWorkspaceSandbox gates the sandbox to exactly those scope and mode combinations.
Specific external cache tools fail in a live authenticated thread, and a permission-rule workaround restores them.UnverifiedNo real provider credentials or personal settings were used. The report verifies the preceding configuration defect directly.

3. Environment

4. Minimal reproduction

  1. Clone trusted get-bb/bb main and check out c4991dae45d3ddbbca2c485dbebb73894c568e68.
  2. Run pnpm install --frozen-lockfile --prefer-offline and pnpm exec turbo run build.
  3. Copy the regression test to plugins/provider-claude-code/src/bridge/__tests__/sandbox-user-settings.repro.test.ts.
  4. Run pnpm exec turbo run test --filter=bb-plugin-provider-claude-code --force.

The test creates a synthetic HOME, writes one user sandbox path, asks bb to add a different root, and asserts that both survive:

import { afterEach, describe, expect, it } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildSessionOptions } from "../session-options.js";

const tempDirs: string[] = [];

afterEach(() => {
  for (const directory of tempDirs.splice(0)) {
    rmSync(directory, { force: true, recursive: true });
  }
});

describe("Claude workspace sandbox user settings", () => {
  it("preserves the user allowWrite paths when adding bb write roots", () => {
    const homeDir = mkdtempSync(join(tmpdir(), "bb-claude-settings-"));
    tempDirs.push(homeDir);
    const settingsDir = join(homeDir, ".claude");
    const userCache = join(homeDir, "cache");
    const bbWriteRoot = join(homeDir, "thread-storage");
    mkdirSync(settingsDir);
    writeFileSync(
      join(settingsDir, "settings.json"),
      JSON.stringify({
        sandbox: { filesystem: { allowWrite: [userCache] } },
      }),
    );

    const options = buildSessionOptions(
      {
        additionalWorkspaceWriteRoots: [bbWriteRoot],
        chromeEnabled: false,
        cwd: join(homeDir, "workspace"),
        getPermissionEscalation: () => "deny",
        instructionMode: "append",
        permissionMode: "auto",
        permissionScope: "workspace",
        workflowsEnabled: false,
      },
      { HOME: homeDir, PATH: "" },
    );

    expect(options.sandbox?.filesystem?.allowWrite).toEqual([
      userCache,
      bbWriteRoot,
    ]);
  });
});

Expected: the test passes with both paths.

Actual, clean run A:

FAIL src/bridge/__tests__/sandbox-user-settings.repro.test.ts
AssertionError: expected [ Array(1) ] to deeply equal [ ... (2) ]

- Expected
+ Received

  [
-   "/tmp/bb-claude-settings-VGPyFS/cache",
    "/tmp/bb-claude-settings-VGPyFS/thread-storage",
  ]

Test Files  1 failed | 25 passed (26)
Tests       1 failed | 356 passed (357)

Artifacts: first clean run · second clean run

5. Root cause

buildWorkspaceWriteSandbox derives allowWrite only from additionalWorkspaceWriteRoots and returns that array as the complete SDK sandbox filesystem configuration:

const allowWrite = params.additionalWorkspaceWriteRoots ?? [];
return {
  enabled: true,
  failIfUnavailable: false,
  autoAllowBashIfSandboxed: true,
  allowUnsandboxedCommands: true,
  network: { allowLocalBinding: true },
  ...(allowWrite.length > 0
    ? { filesystem: { allowWrite: [...allowWrite] } }
    : {}),
};

buildSessionOptions forwards that object without reading settings from the home directory. Later, SdkSession.start simultaneously enables the filesystem settings sources and forwards the explicit sandbox. Thus loading the user source does not help bb construct the explicit array it hands to the SDK.

The host daemon supplies only workspace-related roots at runtime-manager.ts. There is no path in this flow that reads and validates a user's Claude sandbox configuration before the provider options are assembled.

6. Proposed fix (first principles)

At the provider's host-side settings boundary, parse the Claude settings sources that bb enables, preserve their documented precedence, and union array-valued sandbox grants with bb's additional workspace roots before building the SDK sandbox object. Keep scalar restrictions and deny lists under their existing precedence rather than treating every sandbox field as additive. Add focused tests for a user-only writable root, user plus bb roots, malformed settings, and conflicting restrictive fields. This changes a permission boundary, so the exact merge policy should be reviewed deliberately rather than landed as an unattended small fix.

7. Related issues

No linked open pull request was present, and no other issue was independently reviewed for this report.

8. Verification

A second clean clone was checked out at the same full commit, installed with the frozen lockfile, and built successfully. The identical regression test and Turbo command then failed in the same way: the received array contained only the synthetic thread-storage root, with the synthetic user cache path missing. The second run had 1 failed and 356 passing tests across 26 test files. No report claim required correction.

9. Appendix

Commands used:

git clone --branch main --single-branch git@github.com:get-bb/bb.git <checkout>
git checkout --detach c4991dae45d3ddbbca2c485dbebb73894c568e68
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=bb-plugin-provider-claude-code --force
git fetch origin main
git log --oneline c4991dae45d3ddbbca2c485dbebb73894c568e68..origin/main -- <relevant paths>

The final history check found no later commit on origin/main touching the relevant provider and host paths. The issue text, comments, links, and code blocks were treated as untrusted claims; no command, patch, branch, external link, or credential from the issue was executed or used.