Badge terminal rows that opt out of the sandbox - #3038
Open
andrewkchan wants to merge 1 commit into
Open
Conversation
Claude Code's Bash tool takes a dangerouslyDisableSandbox flag, and bb enables the SDK sandbox itself for workspace-scoped acceptEdits/auto sessions. The timeline showed no difference between a command that ran sandboxed and one that escaped, so an unsandboxed call was indistinguishable from its neighbours. parseClaudeBashCommand now reads the flag alongside run_in_background, and the delta translator carries the session's sandbox state so commandPresentation can tell "opted out of an active sandbox" from "session never had one". Only the former gets a badge. Presentation grows an optional badge (glyph, compact label, hover hint, tone), rendered by the timeline title layer as a new decoration: an icon in the app, "(sandbox off)" in the CLI. Bash-driven grep/ls rows are command rows with parsed intents, so terminal, search, and list rows are all covered. Fixes a pre-existing projection bug found while verifying this: the commandExecution branch of parseExecLifecycleEvent never copied item.presentation onto the projected call, unlike the delegation and tool-call branches. Every command row reached the UI with no presentation. Nothing consumed it before, since command titles skip the presentation path, so the badge was persisted correctly and then dropped in projection. HOST_DAEMON_PROTOCOL_VERSION 173 -> 174. The bridge artifact is served by the server, so a new bridge can run under an old daemon; that daemon parses each thread/delta with its own copy of the schema, and since presentation is a non-strict z.object it would silently strip the badge before posting the event the server persists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andrewkchan
force-pushed
the
sandbox-escape-badge
branch
from
September 4, 2026 00:38
863943a to
b33704c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human comments
Adds a red "unlocked" badge next to claude code terminal commands that run outside of sandbox:

What was wrong
Claude Code's Bash tool accepts a
dangerouslyDisableSandboxflag, and bb turns the SDK sandbox on itself for workspace-scopedacceptEdits/autosessions (buildWorkspaceWriteSandbox). The timeline rendered a command that ran sandboxed and one that deliberately escaped identically, so the security-relevant call was indistinguishable from its neighbours.parseClaudeBashCommandreadrun_in_backgroundoff the tool input and dropped the sandbox flag next to it, so the signal never left the provider bridge.While verifying the fix in the dev app I found a second, pre-existing bug: the
commandExecutionbranch ofparseExecLifecycleEventnever copieditem.presentationonto the projected call, unlike the delegation (exec-lifecycle.ts:222) and tool-call (:294) branches. Every command row reached the UI withpresentation: undefined. Nothing had consumed it before —mapExecutionTitleskips the presentation path for commands — so the badge was persisted correctly on the event and then silently dropped in projection.What changed
Sandbox signal (
plugins/provider-claude-code) —parseClaudeBashCommandparsesdangerouslyDisableSandboxalongsiderun_in_background;createClaudeDeltaTranslatortakes a requiredsandboxEnabled, fed fromsessionOptions.sandbox?.enabledin the bridge, soclassifyClaudeToolUsecan distinguish "opted out of an active sandbox" from "session never had one". Only the former gets a badge.Contract (
packages/domain/src/item-presentation.ts) — presentation grows an optionalbadge:glyph, a compactlabel, ahintfor hover/screen-reader text, andtone. Available to any provider bridge, not just this one.Rendering (
packages/thread-view,apps/app) — a newbadgetitle decoration derived from the row presentation, added in the command-title path, the single-exploration-intent path, and the compact multi-intent path (first line only, so a compound command doesn't repeat it). The app renders it as a destructive-toned icon withtitle/aria-labelset to the hint; the CLI gets(sandbox off)for free throughtitle.plain. Bash-drivengrep/lsrows are command rows with parsed intents, so terminal, search, and list rows are all covered by one change.Projection fix (
packages/thread-view/src/exec-lifecycle.ts) — one line copyingitem.presentationonto command calls, mirroring the sibling branches.Wire change:
HOST_DAEMON_PROTOCOL_VERSION173 → 174. The bridge artifact is served by the server, so a new bridge can run under an old daemon. That daemon parses eachthread/deltawith its own compiled copy of the schema (bridge-protocol-adapter.ts:513), and since presentation is a non-strictz.objectit strips unknown keys rather than rejecting — verified: an old shape accepts the new payload andbadgecomes backnull. Without the bump the badge would be silently swallowed before the event is persisted, with no error anywhere.No CLI command, flag, or config knob changed. Mobile renders none of the title decorations, so the badge is web/CLI only — the same gap
docs/api_to_audit.md:1435already tracks for the presentation base.How you verified
Tests added:
plugins/provider-claude-code/src/presentation.test.ts— badge present/absent across the three input × session-sandbox combinations.packages/thread-view/test/timeline-row-title.test.ts— the decoration reaches both title paths, and only the first compact intent line.packages/thread-view/test/v3-item-projection.test.ts— acommandExecutionitem with a badge presentation driven through the real projection, asserting bothrow.presentationand(sandbox off)in the rendered title. Confirmed this fails with theexec-lifecycle.tsfix reverted and passes with it.apps/app/.../ThreadTimelineRows.presentation.test.tsx— the icon renders on both a command row and a search row, with the hint ontitleandaria-label, and not on an unbadged row.Commands run:
pnpm exec turbo run typecheck— 80/80 packages pass.pnpm exec turbo run test --filter=@bb/thread-view --filter=bb-plugin-provider-claude-code --force— 370 and 337 tests pass.pnpm exec turbo run test --filter=@bb/app --force— 3507 pass, 3 skipped.oxfmt --checkclean on all 16 changed files.Manual check in the dev desktop app: ran the same command with and without the sandbox, confirmed via the dev SQLite DB that the badge is persisted on the escaped call only.