#3034 · Claude sandbox omits user writable paths when bb adds roots
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
| Claim | Status | Evidence |
|---|---|---|
| bb's workspace sandbox replaces the effective user writable-path list with its own additional roots. | Verified | Both 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. | Verified | sdk-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. | Verified | usesWorkspaceSandbox 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. | Unverified | No real provider credentials or personal settings were used. The report verifies the preceding configuration defect directly. |
3. Environment
- Trusted repository:
get-bb/bbatc4991dae45d3ddbbca2c485dbebb73894c568e68. - macOS 26.6.1 (Darwin 25.6.0, arm64), Node v22.22.3, pnpm 9.15.0.
@anthropic-ai/claude-agent-sdk0.3.245 from the frozen lockfile.- No dev server, ports, persistent data directory, real home settings, or provider session were used.
- The full monorepo build passed independently in each clean checkout before the reproduction test was added.
4. Minimal reproduction
- Clone trusted
get-bb/bbmain and check outc4991dae45d3ddbbca2c485dbebb73894c568e68. - Run
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build. - Copy the regression test to
plugins/provider-claude-code/src/bridge/__tests__/sandbox-user-settings.repro.test.ts. - 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.