You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Crewmates today are plain same-UID host processes launched with every harness safeguard off (--dangerously-skip-permissions, --dangerously-bypass-approvals-and-sandbox, --yolo; the muse adapter even disables muse's built-in sandbox so git/network work). The only isolation between tasks is a treehouse git worktree, which prevents branch collisions but nothing else. bin/fm-spawn.sh says so itself in the launch-env header:
This is an exec environment boundary, not a sandbox for the pane's startup shell, credential files, same-user processes, or later shell initialization.
Upstream's position (kunchenguid#559, kunchenguid#1426) is that safety "needs to be established in the environment where we run our agents", not by reviewing tool calls. We agree — and want to actually build that environment: keep full bypass inside a container, so the blast radius of a crewmate becomes its worktree plus what we deliberately mount.
Proposal: a default-off config/crew-container that wraps the existing per-harness launch string in a container runtime exec. Nothing changes when the file is absent (byte-identical launch), so this is class=opt-in under upstream's triage rules.
What a crewmate can reach today that a worktree does not protect
All crewmates share the captain's UID with unrestricted tool execution:
PR Enforce crew Claude sandbox posture at spawn kunchenguid/firstmate#3380 (open) — config/crew-claude-config-dir + a required worker-sandbox-posture-check.sh that refuses to launch a Claude worker unless a sandbox posture is proven. Triaged favorably. It is a gate with nothing yet that satisfies it; this proposal is the thing that would satisfy it.
A crewmate is not a black box; firstmate talks to it through the host filesystem and process table. From bin/fm-spawn.sh on upstream main:
Linked git worktree. The task slot's .git is a file pointing at the project clone's .git/worktrees/<slot> (spawn resolves --git-common-dir, ~:2508). The container must mount both the slot and the common git dir at the same absolute paths (git stores absolute gitdir/commondir pointers).
Hook callbacks by absolute host path. The per-task $WT/.claude/settings.local.json (~:3514) wires UserPromptSubmit/Stop/StopFailure/SessionEnd to $FM_ROOT/bin/fm-busy-event.sh apply $STATE_REAL $ID … and touch $TURNEND. Other harnesses have equivalents (grok .grok/hooks, gemini .gemini/settings.json, opencode plugin, kimi Stop hook). Inside the container $FM_ROOT/bin (ro) and $FM_ROOT/state (rw, ideally only this task's files) must resolve.
Agent-state classification walks the pane's process tree with ps (bin/fm-backend.sh ~:214-221; relaunch path in bin/fm-control.sh). With docker run -it in the pane the tree is the docker CLI, not claude. The classifier must learn the wrapper shape or relaunch/liveness will misclassify — this is the one non-additive change.
Delivery needs network + scoped creds.git push, gh pr create, no-mistakes. Inject a scoped GH_TOKEN and a git credential helper via the env allowlist; never mount ~/.ssh or ~/.config/gh.
Secondary: a per-project image carrying the toolchain (node, gh, no-mistakes, the harness CLI), cold-start cost, macOS bind-mount performance.
Proposed shape
config/crew-container (one token: docker | podman | container; absent = unchanged). Possibly config/crew-container-image / config/crew-container-args for the image and extra mounts.
bin/fm-container-lib.sh: given the already-composed adapter launch string, emit <runtime> run --rm -it <mounts> <env> <image> /bin/sh -c '<launch>'. Reuse config/launch-env-allowlist for the env contract.
bin/fm-backend.sh: recognise the container CLI as a transparent wrapper when walking the process tree (or classify via docker inspect/docker top of a task-labelled container).
Applies to ship and scout spawns and relaunch. Secondmates out of scope for v1 (they already have a separate-home/separate-host boundary).
Not a new backend: backends are session providers (tmux/herdr/zellij/cmux/orca); containment is orthogonal to where the pane lives.
Tests: fake-docker behavioral coverage in tests/, asserting (a) absent config → unchanged launch bytes, (b) mounts and env for each coupling above, (c) classifier recognises the wrapper.
Estimated size: roughly PR kunchenguid#3380's footprint (~400–800 lines incl. tests), an order of magnitude smaller than kunchenguid#2241.
Plan
Shape-A experiment (no code):fm-spawn.sh accepts a raw launch command as the harness arg (~:1723, "unverified-adapter escape hatch"). Run a crewmate with a hand-written docker run … -v $WT:$WT -v <common-gitdir>:<same> -v $FM_ROOT/bin:…:ro -v $FM_ROOT/state:… -e GH_TOKEN … claude --dangerously-skip-permissions and record which of the four couplings hold. Expected: 1, 2, 4 work with same-path mounts; 3 breaks; --model/--effort threading is lost because raw launch bypasses the adapters.
Implement the opt-in wrapper on this fork behind config/crew-container.
Summary
Crewmates today are plain same-UID host processes launched with every harness safeguard off (
--dangerously-skip-permissions,--dangerously-bypass-approvals-and-sandbox,--yolo; the muse adapter even disables muse's built-in sandbox so git/network work). The only isolation between tasks is a treehouse git worktree, which prevents branch collisions but nothing else.bin/fm-spawn.shsays so itself in the launch-env header:Upstream's position (kunchenguid#559, kunchenguid#1426) is that safety "needs to be established in the environment where we run our agents", not by reviewing tool calls. We agree — and want to actually build that environment: keep full bypass inside a container, so the blast radius of a crewmate becomes its worktree plus what we deliberately mount.
Proposal: a default-off
config/crew-containerthat wraps the existing per-harness launch string in a container runtime exec. Nothing changes when the file is absent (byte-identical launch), so this isclass=opt-inunder upstream's triage rules.What a crewmate can reach today that a worktree does not protect
All crewmates share the captain's UID with unrestricted tool execution:
~/.ssh,~/.config/ghtoken,~/.gitconfig,~/.claude,~/.aws, ….gitdir (objects/,refs/,config,worktrees/)git gc/prune/worktree remove/configin one slot hits all siblings and the primary checkout../<other-slot>/)cd ../other && edit— the cd-guard is inert inside worktrees by design$FM_ROOT/state/, the watcher process, other paneskillthe watcher, type into siblings$HOMErm -rfoutside cwd, install/mutate shared toolchainPrior art (upstream)
config/crew-claude-config-dir+ a requiredworker-sandbox-posture-check.shthat refuses to launch a Claude worker unless a sandbox posture is proven. Triaged favorably. It is a gate with nothing yet that satisfies it; this proposal is the thing that would satisfy it.-s workspace-write, Cursor--sandbox enabled, Claude--permission-mode auto). Triagedclass=new-default, stalled. Also per-harness, so every adapter needs its own story; a container wrapper is harness-agnostic.outcome=ready-for-pr class=opt-in) — bound ambient env/secret inheritance at launch.config/launch-env-allowlistalready exists and is the natural env contract for the container.The four host couplings a wrapper must preserve
A crewmate is not a black box; firstmate talks to it through the host filesystem and process table. From
bin/fm-spawn.shon upstream main:.gitis a file pointing at the project clone's.git/worktrees/<slot>(spawn resolves--git-common-dir, ~:2508). The container must mount both the slot and the common git dir at the same absolute paths (git stores absolutegitdir/commondirpointers).$WT/.claude/settings.local.json(~:3514) wiresUserPromptSubmit/Stop/StopFailure/SessionEndto$FM_ROOT/bin/fm-busy-event.sh apply $STATE_REAL $ID …andtouch $TURNEND. Other harnesses have equivalents (grok.grok/hooks, gemini.gemini/settings.json, opencode plugin, kimi Stop hook). Inside the container$FM_ROOT/bin(ro) and$FM_ROOT/state(rw, ideally only this task's files) must resolve.ps(bin/fm-backend.sh~:214-221; relaunch path inbin/fm-control.sh). Withdocker run -itin the pane the tree is the docker CLI, notclaude. The classifier must learn the wrapper shape or relaunch/liveness will misclassify — this is the one non-additive change.git push,gh pr create,no-mistakes. Inject a scopedGH_TOKENand a git credential helper via the env allowlist; never mount~/.sshor~/.config/gh.Secondary: a per-project image carrying the toolchain (node, gh, no-mistakes, the harness CLI), cold-start cost, macOS bind-mount performance.
Proposed shape
config/crew-container(one token:docker|podman|container; absent = unchanged). Possiblyconfig/crew-container-image/config/crew-container-argsfor the image and extra mounts.bin/fm-container-lib.sh: given the already-composed adapter launch string, emit<runtime> run --rm -it <mounts> <env> <image> /bin/sh -c '<launch>'. Reuseconfig/launch-env-allowlistfor the env contract.bin/fm-backend.sh: recognise the container CLI as a transparent wrapper when walking the process tree (or classify viadocker inspect/docker topof a task-labelled container).dockerbehavioral coverage intests/, asserting (a) absent config → unchanged launch bytes, (b) mounts and env for each coupling above, (c) classifier recognises the wrapper.Estimated size: roughly PR kunchenguid#3380's footprint (~400–800 lines incl. tests), an order of magnitude smaller than kunchenguid#2241.
Plan
fm-spawn.shaccepts a raw launch command as the harness arg (~:1723, "unverified-adapter escape hatch"). Run a crewmate with a hand-writtendocker run … -v $WT:$WT -v <common-gitdir>:<same> -v $FM_ROOT/bin:…:ro -v $FM_ROOT/state:… -e GH_TOKEN … claude --dangerously-skip-permissionsand record which of the four couplings hold. Expected: 1, 2, 4 work with same-path mounts; 3 breaks;--model/--effortthreading is lost because raw launch bypasses the adapters.config/crew-container.Out of scope