Development record
Unattended runs abcd launches execute inside an OS sandbox
Context
adr-42 decided that abcd's command guard is a mistake filter and that anything needing an enforced boundary needs an execution-layer control behind it. This record applies that decision to the one place abcd itself now runs agents nobody watches.
abcd starts a process only for a role the person has routed to a runner on their own machine, and it does so at one launch point, internal/core/runner/proc.go. The launched agent runs without a terminal, in print mode with the bare flag, with its role's tools granted up front and every other tool denied, because no one is there to answer a prompt. What confines it today is that tool list and nothing else: an allowed edit or shell command reaches whatever the person's account can write. The bare flag also means the launch loads no hooks, so abcd's guard does not reach it; iss-2610161612203555, being built now, restores the guard per launch.
Everything else abcd calls autonomous runs elsewhere. A drain or build is driven by the person's own session on the host route by default (internal/core/implement/loop/drive.go), under the host's settings, which abcd never edits (dcn-2610161612269647). For those runs abcd can detect and report how isolated they are, and cannot fence them.
The host's own documentation sets the bar for the unattended case. Its shell sandbox covers shell commands only, while the file tools, hooks and servers run outside it, and it calls that sandbox on its own "not sufficient for fully unattended runs"; a run without prompts belongs inside a container, a virtual machine, or a sandbox around the whole process (sandboxing, sandbox environments).
On 2026-10-16 the product thinker named sandboxing these runs an important feature (dcn-2610161619296615, which corrects dcn-2610161612269647's "does not sandbox"), and intent itd-2610161621364075 asks for it.
Decision
We will run every agent abcd's runner launches inside a boundary the operating system enforces around the whole launched process, so that its file tools, hooks and servers are confined along with its shell commands.
We will derive what the launched agent may write from the run itself: its worktree; its receipt folder in the main checkout's local tier, with the run's state readable only; the main checkout's git directory without its hooks and configuration; the Go caches, read-only or one per run; the temporary directory; and the host's own state, with the paths the host loads configuration from held read-only. abcd's home stays readable to the agent and writable only at that agent's own worktree, so the next launch's policy (the runner routes, the rules, the trusted roots) is the person's alone to change.
We will run the machine-load gate and every other check that reads the machine in abcd, before the launch and outside the boundary, where the whole machine is visible.
We will keep abcd's guard teaching inside the boundary, once iss-2610161612203555 brings it to each launch, as the mistake filter adr-42 describes.
We will say, before a run's first launch, whether its launches will be confined, and name the reason when the machine offers no boundary. Where a person chooses to run without one, that choice lives in their own user layer under ~/.abcd.noindex/, as trusted-roots does.
We will describe a launch as confined only where the boundary demonstrably runs, per enforcement-claims-are-facts.
We will decide limiting the network as a separate, later record.
Alternatives Considered
- The status quo: the per-launch tool grant and the guard. It costs nothing and already narrows which tools a launched agent may call. Rejected because it decides which tools run and not what they reach: an allowed edit or command acts with the account's full reach, and the guard, a mistake filter by adr-42, does not reach the launch at all until iss-2610161612203555 lands.
- A stricter guard. adr-42 records a fail-closed allowlist as the shape a stricter guard would take, and it would tighten shell commands. Rejected as the boundary because the guard reads shell commands only, so the file tools pass it untouched, and what an allowed command does once running is outside what it reads.
- Per-launch approval alone (iss-2610161612203555's approval tool, backed by the guard's registry). Kept as the first step for answering the host's prompts. Rejected as the boundary because approval is a decision before each call, not a confinement during it: it does not confine the file tools, or anything an approved command goes on to start.
- A whole-process sandbox at the runner's launch point. Anthropic's sandbox-runtime wraps a process in Seatbelt on macOS and bubblewrap with a host-side proxy on Linux, and denies writes to
.git/hooksand.git/configon its own (sandbox-runtime); it is a beta research preview, Apache-2.0, and a new dependency that needs explicit sign-off. abcd's own Seatbelt and bubblewrap or Landlock (Landlock) profiles avoid the dependency and carry their maintenance. Either meets the decision; the intent chooses. - The host's own sandbox, passed per launch with
--settings(sandboxing, headless). It leaves the person's settings untouched and is maintained by the host's vendor. Rejected as the boundary because it covers shell commands only, and it is one runner's mechanism, not the other's; it remains a possible inner layer. - A container, a virtual machine, or a cloud session. The strongest separation, with its own kernel in the last two. It ties abcd to a container runtime or a hosted service, and sharing the worktree and receipt folder with abcd across that boundary is harder. A candidate the intent weighs beside the whole-process sandbox.
Consequences
- The machine-load gate and every
pgrep-style waiter run in abcd, outside the boundary: inside a sandbox the process table is not the machine's: on macOSpscannot start at all andpgrepsees no process (the spec's step 1 evidence, 2026-10-17), and elsewhere it may see only the sandbox's own, so a check run there cannot read the machine. - A run without a boundary is the person's choice, recorded in their user layer under
~/.abcd.noindex/; no repository file can make it for them. - Network limiting is deferred to its own record. When it comes, its honest claim is a declared list of hosts, which narrows mistakes and does not stop code being sent elsewhere: allowing the forge allows a push to any repository the credential reaches.
- Drains and builds a person's own session drives stay unconfined by abcd; reporting their isolation is proposed as a separate part of the intent.
- The guard keeping its place inside the boundary depends on iss-2610161612203555.
- Platform limits become the intent's scope conditions: Go tools' TLS under Seatbelt, git over SSH behind a proxy, the keychain credential helper,
sandbox-exec's deprecation on macOS, nested bubblewrap, and Ubuntu's restriction on unprivileged user namespaces. - A brief invariant states the rule once this record is accepted.
- The boundary sits behind a seam (
internal/core/fence): a fence-neutral policy and one interface each mechanism implements, so the mechanism the spec picks (sandbox-runtime,spc-2610161751056096) can be replaced by another without changing the runner or the loop. - The mechanism is a program abcd does not ship, and a boundary has no native default to degrade to, so on the runner route it is a requirement: adr-22's first declared exception, signed off by the product thinker on 2026-10-22 (dcn-2610222041417934), with the person's off switch in their user layer in place of degradation.
- A signing key is not opened to a confined agent. Where the person's commits are signed, a confined agent commits unsigned and abcd signs outside the boundary when it lands the work (the product thinker's ruling of 2026-10-22, dcn-2610222041439951).
Frontmatter
| id | adr-2610161621415708 |
|---|---|
| type | adr |
| status | proposed |
| date | 2026-10-16 |
.abcd/development/decisions/adrs/2610161621415708-abcd-s-command-guard-steers-agents-an-os.md
open on GitHub ↗ · commit history ↗
References2
- adr-42The guard's parse layer is a mistake filter, not a security boundary — matching is two-tier, and completeness is abandoned as a goalrelated
- itd-2610161621364075The agents abcd launches on its own work inside a sandbox the operating system enforcesrelated
Referenced by1
- adr-2611011813190527A repository declares a sequence's shape but never a widening flagrelated
mentions10
- adr-22Bundled dependencies become pluggable adapters over a native default
- dcn-26101616122696472026-10-16 — Agents keep running past the host's permission prompts in two ways (product thinker, 2026-10-16): abcd teaches agents the command forms the host can check (iss-2610161612200864), and, for the agents abcd itself starts without a terminal, abcd answers the prompts per launch with its guard rules (iss-2610161612203555). abcd does not approve commands in interactive sessions, does not sandbox, and never edits the person's host settings.
- dcn-26101616192966152026-10-16 — Correction to dcn-2610161612269647: sandboxing autonomous runs was not declined. The 2026-10-16 question offered only teaching and per-launch approvals, after the sandbox option was dropped to fit the screen, so the product thinker made no ruling on it; it stays open, and the product thinker named it on the same day as an important feature for autonomous runs.
- dcn-26101810034564442026-10-18 — Only the person sets the away mode, its push rights are narrow, and the guard's refusal is a mistake filter (facilitator-on-delegation, itd-2610180912189125 review round 2, choice (a); refines dcn-2610180913399042, replacing its clause that an agent sets the mode on the person's word): the prompt hook cannot tell the person's message from one another session relays, and its input names no origin, so a relayed word could let an agent grant itself, and every copy, push rights. So the guard refuses every route to setting the mode that it reads: `abcd mode autonomous` by the binary on PATH, the plugin root's binary or a built `abcd-<goos>-<arch>`; a `go run` of a package ending in `cmd/abcd`; any of them inside `sh -c` or in a script file a shell runs (adr-2610091150447054); a shell write into the away store under `~/.abcd.noindex/away/`; and an Edit or Write tool call on a path inside it. The person sets the mode with their own `!` command, which the host runs directly without going through Claude and so without the pre-tool hook, or at a terminal. A refused attempt is recorded in the store and shown to the person on the next prompt. What the mode grants is the session committing and pushing its own branches and opening their pull requests; never the default branch, a tag, a release cut, another person's pull request, a branch this copy only checked out to review, or a force push; merging follows the repository's own rule. This is chosen over the fallback, and is enough for now, because the grant only governs an agent that follows refusals: one that ignores them can push without any mode, so no shape of the mode adds a boundary inside the person's own session; the guard stops a refusal-following agent from granting itself on a relayed word; the rights are narrow, and every push is reversible and visible on the forge; and the record (who set it and from which copy) and the recorded refusals leave a trail the person reads on return. The guard stays a mistake filter (adr-42, and the decision of 2026-10-17 on the default branch that keeps the headless approver one, naming the OS fence as the boundary). That fence (spc-2610161751056096) is the boundary only for agents abcd's runner launches, whose writes to abcd's home it confines to their own worktree; it does not fence the person's own interactive session, where the stall happened, so there the narrow grant and the trail are the mitigation. The fallback (the mode grants no push rights, and the person's own standing instruction stays the authority to push) applies instead if the live capture shows that a message relayed from another session can run as a `!` command, or that an agent's tool call can reach the host's shell mode (grounds: plan review round 2, choice (a), and the coordinator's two addenda; the host's interactive-mode page, which says shell mode runs commands directly without going through Claude and outside the sandbox that applies to Claude's commands; the abcd-source-ledger-flip guard entry; adr-2610161621415708)
- dcn-26101810165101532026-10-18 — The away mode grants no push rights: the person's own standing instruction stays the authority to commit, push, and open or merge a pull request, exactly as before the mode existed (facilitator-on-delegation on the release orchestrator's ruling, itd-2610180912189125 after plan review round 2; supersedes dcn-2610181003456444, round 2's choice (a), and with it the guard entries that made setting the mode the person's act alone). The OS sandbox of spc-2610161751056096 fences only the agents abcd's runner launches, never the person's own interactive sessions, so for the sessions that actually stalled on 2026-10-16 the guard entries would never become a boundary, and their two days would buy a trust claim the plan cannot make. The fallback dcn-2610181003456444 already named is taken now, not on the live capture's evidence. Where the stop check finds owed work that needs a push (an unpushed branch, a reservation whose work no pull request carries, a behind or failing pull request once its local fix is committed), its reason tells the session to push if its standing instruction allows, and otherwise to queue the push with abcd mode queue for the person's return; a queued step that names the item's branch or record id, on word boundaries, hands that item back to the person for the rest of the window (a branch only while it holds commits its upstream lacks, so only the push waits), and the stop check stops naming it. The away mode is never treated as permission. The person-only acts gain pushing, opening or merging a pull request without a standing instruction that allows it. Who sets the mode returns to dcn-2610180913399042: the person, or a session on their word in that session; since the mode grants nothing, a mode set on a relayed word only keeps sessions working and deciding instead of asking, and the person sees who set it, from which working copy, and the literal abcd mode back on their next prompt. Making the mode the person's act alone moves to itd-2610170638184486 as named remaining work: the guard entry refusing an agent's abcd mode autonomous, its other routes (sh -c and script files), the shell-write and file-tool refusals on the away store, the refused attempts recorded and shown, the capture's checks of the person's ! command and of a relayed !, and the guard reading go run ./cmd/abcd and built abcd-<goos>-<arch> binaries as abcd for every entry, the ledger flip included, a security-relevant widening that gets its own review there (grounds: the release orchestrator's ruling of 2026-10-18 and the reviewer's addendum, both recommending the fallback; spc-2610161751056096 and adr-2610161621415708, which fence launched agents only; iss-2610170619247185, whose standing instruction already allowed pushes and auto-merge; the fallback clause of dcn-2610181003456444)
- dcn-26102220414179342026-10-22 — Anthropic's sandbox (srt) is required for the agents abcd starts on its own (product thinker's ruling of 2026-10-22, spc-2610161751056096 step 2 question a, itd-2610161621364075): every agent the runner launches runs inside srt, on macOS and Linux, and on a machine without it no such agent starts; this is adr-22's first declared exception, scoped to the runner route alone, and the per-machine off switch runner.sandbox.off stays the person's way out. The proposed marks on adr-22's consequence and on the dependencies chapter's paragraph come off
- dcn-26102220414399512026-10-22 — abcd signs a fenced lane's commits when it hands the work in (product thinker's ruling of 2026-10-22, spc-2610161751056096 step 2 question d): lane commits are made unsigned inside the fence, with commit.gpgsign=false in the launch's seal; at land, outside the fence, abcd rewrites each lane commit with the same tree, message and order, signed under the person's own configuration, and the judged-head check compares the rewritten chain tree for tree with the head its validators judged, so the agent never holds the signing key
- iss-2610161612203555An agent abcd starts without a terminal has nobody to answer the host's permission prompts. A command the host cannot check is then refused, and the lane stalls or fails. The product thinker chose to have abcd answer those prompts for the agents it starts, using its existing guard rules.
- itd-2611011813155166A repository switches an abcd verb or project gate off
- spc-2610161751056096a-person-who-leaves-abcd-to-run-on-its