arbiterforge/sandbox-lifecycle
The lifecycle gate for a local Codespace-equivalent sandbox. Routed to when the user invokes /ca-sandbox:sandbox to pull an untrusted repo into an ephemeral, host-FS-isolated Docker container, or any of the interaction commands (/ca-sandbox:sandbox-shell, /ca-sandbox:sandbox-exec, /ca-sandbox:sandbox-cp, /ca-sandbox:sandbox-destroy) against an existing box. Five gated phases — pre-flight, clone+build, isolated run, interact, teardown. The load-bearing invariant is structural: untrusted code in the box can never reach the host filesystem (no bind mount, no docker socket, never --privileged, cap-drop ALL, non-root, read-only root). Network defaults to offline; egress out is host-initiated only. Every object is labeled ca.sandbox=1 and torn down on exit.
npx skills add https://github.com/arbiterForge/codeArbiter --skill sandbox-lifecycle
Pull an untrusted repo into a throwaway box, explore it without risking the host, then burn the box. This skill owns the whole arc — clone into a named volume, build a dep-cached image, run it under structural isolation, interact (shell / exec / cp out), destroy — and the one invariant that makes it safe: the code inside the box can never touch the host filesystem. That guarantee is enforced by construction (no bind mounts, no docker socket, never --privileged), not by trusting the repo.
The driver lives in ${CLAUDE_PLUGIN_ROOT}/tools. The skill never hand-rolls a docker run argv — every container is started through runContainer in ${CLAUDE_PLUGIN_ROOT}/tools/run.ts, whose mount argv comes only from buildMountArgs in ${CLAUDE_PLUGIN_ROOT}/tools/mounts.ts (the chokepoint that throws on any bind spec).
Read these, or STOP and surface the gap — never guess a Docker capability, a mount layout, or an egress posture:
${CLAUDE_PLUGIN_ROOT}/tools/mounts.ts — the mount-arg chokepoint. Every mount is built here; it throws (BindMountRejectedError) on any type=bind spec. The structural half of the host-FS invariant.${CLAUDE_PLUGIN_ROOT}/tools/run.ts — the isolation flags (--cap-drop ALL, non-root --user 1000:1000, --read-only, --security-opt no-new-privileges, resource caps) and the offline => --network none default.${CLAUDE_PLUGIN_ROOT}/tools/network.ts — the network policies (offline / clone-then-cut / allowlist). The IP allowlist is EXPERIMENTAL (ALLOWLIST_EXPERIMENTAL); offline and clone-then-cut are the solid defaults.Host prerequisites: Docker and nixpacks on PATH (the plugin's description states this). If docker info fails, STOP and report "Docker is not available" — do not proceed to clone or build. If the user supplies no repo URL to /ca-sandbox:sandbox, ask for one — do not guess a repo.
Establish what is being sandboxed and under what egress posture before any clone:
offline (default), clone-then-cut (fetch deps at build, cut egress at run), or allowlist (EXPERIMENTAL — name it as experimental every time it is selected). Default to offline unless the user names another.docker info returns 0. If not, STOP here.--with-claude — if requested, route to the sandbox-claude-inside skill (${CLAUDE_PLUGIN_ROOT}/skills/sandbox-claude-inside/SKILL.md) for its hardened defaults; it is NOT enabled on the default path.Gate: a named target, a named network policy, and a reachable Docker. A sandbox with no stated target or an unreachable Docker cannot be built — do not improvise either. If allowlist is chosen, the BLOCK is conditional on the user acknowledging it is experimental.
Clone the target into a docker named volume (never onto the host FS, never a bind), then build a dep-cached image:
createSandbox (${CLAUDE_PLUGIN_ROOT}/tools/create.ts); the source lives at /work/repo inside the box.${CLAUDE_PLUGIN_ROOT}/tools/build.ts: nixpacks wraps the repo, deps are relocated out of tree to /deps (exported via NODE_PATH/PYTHONPATH/GOPATH/CARGO_HOME), and the image is tagged ca-sbx:<repo>-<dephash>.computeDepHash (${CLAUDE_PLUGIN_ROOT}/tools/dephash.ts) over the manifest/lockfile set. An unchanged dep set is a cache hit — no rebuild, identical tag. A manifest/lockfile change bumps the dephash and forces a rebuild; a source-only edit does not.Gate: a built (or cache-hit) image tagged ca-sbx:<repo>-<dephash>, with deps at /deps (out of tree). The naive "mount the volume over the app dir" layout shadows baked deps and is forbidden — the volume mounts ONLY at /work/repo. If nixpacks is not installed, STOP with the install hint, not a stack trace.
Start the container through runContainer (${CLAUDE_PLUGIN_ROOT}/tools/run.ts) — never a hand-written docker run. The run carries the structural isolation set, all by construction:
/var/run/docker.sock mount, never --privileged — the three negative guarantees. The mount argv is built only by buildMountArgs, which throws on any bind.--cap-drop ALL, --user 1000:1000 (non-root), --read-only root, --security-opt no-new-privileges, resource caps (--pids-limit, --memory, --cpus)./work/repo; /tmp is a tmpfs (writable scratch, no host backing).offline => --network none; the richer policies are applied by ${CLAUDE_PLUGIN_ROOT}/tools/network.ts.ca.sandbox=1 label (the teardown/registry anchor).Gate: docker inspect on the started container shows no "Type":"bind" mount, no docker-socket mount, and not Privileged:true. If any of the three appears, the run is rejected — there is no override; the chokepoint failed and that is a bug, not a policy decision.
Explore the running box. Each interaction routes to its own command but funnels through this skill's seams:
/ca-sandbox:sandbox-shell) — an interactive shell into the box at /work/repo./ca-sandbox:sandbox-exec) — a single command via execInSandbox (${CLAUDE_PLUGIN_ROOT}/tools/exec.ts), returning a JSON contract: exitCode, separate stdout/stderr, and a truncated flag past the byte cap./ca-sandbox:sandbox-cp) — host-initiated egress ONLY, via cpOut (${CLAUDE_PLUGIN_ROOT}/tools/cp.ts): cp <id>:/work/<f> ./dest over docker cp. The reverse — a host→container bind — is impossible: the mount builder rejects it.Gate: every file leaving the box is host-initiated (docker cp out), never a mount the container could write through to the host. No interaction re-introduces a bind, a socket, or a privilege the run dropped. Exec output honors the byte cap and reports truncated rather than streaming unbounded data.
A sandbox is ephemeral by contract. On exit (/ca-sandbox:sandbox-destroy, or the close of an interactive session):
destroySandbox (${CLAUDE_PLUGIN_ROOT}/tools/destroy.ts) removes the container and its named volume. --keep-volume leaves the volume (for a deliberate re-run); nothing else survives.prune (${CLAUDE_PLUGIN_ROOT}/tools/destroy.ts) reclaims any leaked ca.sandbox=1-labeled object — the safety net for a box whose driver died mid-run.ca-sbx:<repo>-<dephash>) are intentionally retained for the next cache hit; they are excepted from teardown.failures list with docker's own exit code, and a final label-scoped re-list reports whatever is still present. The CLI exits non-zero and names the leftovers.Gate: after a create → interact → destroy cycle, zero ca.sandbox=1-labeled containers or volumes remain (cached images excepted). A run that leaves a labeled object behind without --keep-volume is a leak — prune must be able to find and reclaim it via the label alone. Teardown is verified, not assumed: a docker failure during discovery, removal, or verification means the phase FAILS loudly, because a leaked box is still running untrusted code.
buildMountArgs, which throws on any type=bind. The driver never hand-rolls a -v or type=bind./var/run/docker.sock into a sandbox container, and MUST NOT run one with --privileged. These are non-negotiable structural guarantees, not defaults to override.runContainer with --cap-drop ALL, non-root --user, --read-only root, and --security-opt no-new-privileges. A run missing any of these is rejected./work/repo; deps live out of tree at /deps. MUST NOT mount the volume over the app dir — that shadows baked deps (Spike A) and is the one layout that does not work.offline. The IP egress allowlist is EXPERIMENTAL — name it experimental every time it is selected; offline and clone-then-cut are the solid defaults.docker cp out) only. A host→container bind is impossible and MUST NOT be introduced as a "convenience."ca.sandbox=1, and MUST tear them down on exit (cached images excepted). prune reclaims a leaked labeled object via the label alone.destroy/prune exit non-zero and name every object left behind — automation must never read exit 0 over a still-running untrusted container.--with-claude on the default path — it routes to sandbox-claude-inside, and MUST NEVER co-mount the token volume with an untrusted-code run.Take arbiterforge/sandbox-lifecycle from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference docker.
Without those the skill loads but fails at the first command.