Part IV — The mechanisms (SB10–SB13)¶
Three emitters produce three boundary artifacts. On Linux the launcher stacks with a framework's own mechanism (a claude team on Linux emits both the settings block and the launcher).
SB10 — Mechanism A: Claude native settings-block sandbox ✅/⚙¶
For the claude framework, _sandbox_emit._build_sandbox_block emits a sandbox block into
.claude/settings.hooks.example.json: allowWrite (the write roots), denyWrite (the control-plane
files even inside a write root, plus the whole .claude dir so it cannot be renamed away and
replaced — F-4, mechanism-verified, product-unverified), denyRead/allowRead (for exclusive), and the
allowUnsandboxedCommands: false escape-hatch closure. Claude Code applies it natively once the operator
merges the example into their live settings.json. The block emits no network/egress directive —
claude network confinement is Claude Code's own product default, which agentteams neither emits nor
verifies.
The block is inert until merged — agentteams ships an example, never writes the operator's
settings.json. verify_sandbox_wiring (P1-3) is the read-only, output-only check that the block was
actually merged; it reports booleans and never echoes live-settings secrets.
Honest ceiling. Claude Code's mechanism is verified, and its Linux product arm is verified end-to-end on Linux (2026-09-30: Ubuntu, Claude Code 2.1.251, bubblewrap 0.11.1, socat 1.8.1.1) only after installing Claude Code's documented /etc/apparmor.d/bwrap profile, which disables Ubuntu's bwrap-userns-restrict host-wide; without it sandboxed commands fail closed
(other distros / Claude Code versions untested) — a distinct mechanism from the bwrap launcher of SB12.
Source: agentteams/frameworks/_sandbox_emit.py:176 _build_sandbox_block;
agentteams/frameworks/claude.py:249 (gate), :320 verify_sandbox_wiring.
SB11 — Mechanism B: the native macOS boundaries (goose Seatbelt; claude Seatbelt) ✅/⚙¶
For goose on macOS, goose_sandbox_output_files emits a sandbox.sb Seatbelt profile
(sandbox-exec) that deny file-write* outside the write roots, deny network* by default (an
egress-proxy allow for ONE endpoint is behind an explicit flag), and — for exclusive — deny
file-read* of the protected set. It ships an inert config.yaml.agentteams.example carrying
GOOSE_SANDBOX, which the operator merges. An explicit sys.platform == "darwin" guard ensures this
path emits only on macOS; off macOS it returns [] — not "no boundary" (Linux uses the launcher of
SB12), only Windows has none.
Honest ceiling (#3). The Seatbelt path is enforcement- and profile-syntax-UNVERIFIED off a mac host — a green emission test means "the profile is shaped right," never "it denies on a real mac."
Source: agentteams/frameworks/_goose_sandbox_emit.py:333 goose_sandbox_output_files, :350
(darwin guard); _seatbelt_path_expr.
SB12 — Mechanism C: the framework-neutral launcher (Linux bwrap + macOS sandbox-exec) ✅/⚙¶
The SAME provider-agnostic launcher — repo-root sandbox/confine-run.sh, NOT under .goose/, NOT
framework-gated — carries two OS branches, and base.extra_output_files emits
linux_sandbox_output_files(...) + macos_sandbox_output_files(...) (only one is non-empty per host).
macOS branch (build_macos, 2026-W36) — enforcement-UNVERIFIED (SB20). Once WRAPped, sandbox-exec
+ a generated path-agnostic Seatbelt profile (deny-write outside scratch; deny reads of the
credential/--exclude set; deny-network / loopback-only proxy), RLIMIT_CPU/NPROC caps via ulimit
(--cpu-max/--nproc-max), and a non-exhaustive setuid-exec denylist (compensating hardening — NOT
NoNewPrivs). Honest macOS residuals: memory UNCAPPED (--mem-max is interface-only — a hard cap
needs a VM/container/Linux host); no syscall filtering; sole-proxy egress loopback-only (SBPL
can't pin a remote proxy IP — remote-address control lives out-of-band in PF). The --cpu-max/
--nproc-max/--mem-max flags are no-ops on Linux (cgroups reach those caps OOB). On macOS the
emit also ships sandbox/mac-escape-tests.sh (the on-host deny test that must pass unnested before
the macOS boundary is called "confined") + two INERT Tier-B examples (dedicated-uid-provisioning,
pf-per-tenant-anchor).
Linux branch (bwrap) — enforcement-VERIFIED (SB20). The launcher confines once the operator
WRAPs the invocation with it
(sandbox/confine-run.sh --scratch DIR --egress deny -- <agent cmd>); the emit step only writes the
file — an emitted-but-unwrapped launcher confines nothing (this is why SB12 is ✅/⚙: ✅ as emitted, ⚙
for the operator action that activates it; the ceiling is SB14). When wrapped it enforces:
--ro-bind / /— read-only root;--bind $SCRATCH— the only writable path;--unshare-neton--egress deny— network isolation (--egress proxyruns inside a pre-created netns;--egress hostshares the host net with a printed warning);--tmpfsmasks over the credential set (~/.ssh ~/.aws ~/.gnupg ~/.kube ~/.config/gcloud ~/.azure), applied on every wrap regardless of profile, plus any--excludesibling paths (exclusive);--die-with-parent, the unshare namespaces (user/ipc/pid/uts/cgroup), and NoNewPrivs — which is bubblewrap's default behavior here, not an explicit emitted flag, so unlike the textual flags it is not covered by the SB18 content pin (there is no line to diff);other OS → FAIL CLOSED(never runs a command while labeled "confined").
The launcher content is emitted verbatim (byte-for-byte) from a shipped template asset
(templates/universal/sandbox/confine-run.sh) — it is generic, taking --scratch/--exclude/--egress
at RUN time, so no manifest values are baked in. The emit path lands it repo-root-relative at the correct
../ depth per framework (sandbox_launcher_rel_path) and marks it executable.
Source: agentteams/frameworks/_linux_sandbox_emit.py:52,94; agentteams/frameworks/base.py:125,139;
agentteams/templates/universal/sandbox/confine-run.sh; agentteams/atomicio.py (shebang → +x).
What each mechanism denies (graph G3)¶
flowchart TD
REQ["confined / exclusive team"] --> M{"which mechanism<br/>(by SB7 decision)"}
M -->|"claude (any OS)"| A["A: native settings-block sandbox"]
M -->|"goose (macOS)"| B["B: native Seatbelt sandbox.sb"]
M -->|"any framework, Linux —<br/>bwrap branch (VERIFIED)"| C["C: launcher sandbox/confine-run.sh"]
M -->|"non-claude/non-goose, macOS —<br/>build_macos branch (UNVERIFIED)"| C
A --> WD["WRITE-confine: ALL three<br/>(allowWrite / ro-bind)"]
B --> WD
C --> WD
A --> NA["NETWORK: A emits NO egress directive —<br/>Claude Code product default (unverified, SB10)"]
B --> NBC["NETWORK: B & C deny by default<br/>(deny network* / --unshare-net)"]
C --> NBC
A --> RAB["READ-exclude: A & B only under EXCLUSIVE<br/>(denyRead / deny file-read*)"]
B --> RAB
C --> RC["READ-exclude: C masks the credential set ALWAYS<br/>(~/.ssh … tmpfs/seatbelt, any profile); --exclude adds<br/>siblings under exclusive"]
C --> MAC["macOS-branch residuals (SB12/SB20): mem UNCAPPED ·<br/>no syscall filtering · setuid denylist ≠ NoNewPrivs · loopback-only proxy"]
MAC --> INERT
WD --> INERT["INERT until the operator activates it:<br/>merge settings / set GOOSE_SANDBOX / WRAP (SB14)"]
NA --> INERT
NBC --> INERT
RAB --> INERT
RC --> INERT
SB13 — Framework-neutral wiring, no harness preference ✅¶
The Linux launcher is emitted from base.extra_output_files, so every framework adapter emits it
(claude, codex, copilot-vscode/-cli, agents-md, goose) — no harness is preferred. Only claude.py and
goose.py override extra_output_files, and both call super(); there is no double-emit. The neutral
path resolves to repo-root sandbox/confine-run.sh via sandbox_launcher_rel_path(): ../../ for
2-deep agents dirs (claude/copilot/goose/codex), ../ for 1-deep (agents-md).
Source: agentteams/frameworks/base.py:139; agentteams/frameworks/codex.py,
agentteams/frameworks/agents_md.py (rel-path overrides); claude.py, goose.py (super()).