Part VI — OS confinement¶
OS confinement is the layer that bounds an agent's runtime reach at the operating-system level — below judgment (the sentinel), decisions (the gates), and content (the scanner). It has three parts: a reference model for the defense-in-depth of the deployed system a project builds (S17); the emission of sandbox configuration and the profiles that select it (S18); and the runtime PreToolUse hook that catches agent-initiated actions the CLI gates never see (S19).
Two ceilings govern everything below and are worth a reviewer holding up front:
- Confinement is emitted by default but inert until wired. As of 2026-W39 the
default profile is
confined, under which agentteams emits an OS write-confinement boundary — but only as a settings/config example the operator must merge, so it enforces nothing until wired (S1 fact 5). The PreToolUse hook still stays fail-open by default (its fail-open→fail-closed flip needs an explicitconfined/exclusive, not the defaultedconfined). "OS confinement layer" does not mean "runtime confinement is enforced out of the box." - agentteams emits configuration; it does not enforce it. Enforcement belongs
to the harness (Claude Code's Seatbelt/bubblewrap); the empirically deny-tested path
is Linux (the
sandbox/confine-run.shlauncher), while macOS Seatbelt is UNVERIFIED.
The infrastructure-layers model ✅ (reference doc) {#S17}¶
This section exists to draw one boundary a security reviewer must not blur: infrastructure security ≠ agent security. They are adjacent surfaces, and conflating them is the same error as the design-time/runtime confusion of S1–S2.
The model is a curated eight-layer (L0–L7) defense-in-depth model for the
deployed system a project builds — not for the agent build process — cross-cut
by a design → build → baseline → tune → operate → respond → review lifecycle,
so each layer is considered at each stage:
| Layer | Concern |
|---|---|
| L0 | Governance |
| L1 | Identity |
| L2 | Crypto / secrets |
| L3 | Host & workload hardening — where OS confinement lives |
| L4 | Network |
| L5 | Application & supply chain |
| L6 | Detection / logging |
| L7 | Resilience / backup / IR |
The boundary it draws. This L0–L7 model governs the deployed system; the
@security sentinel governs the build process. The two must not be
collapsed — the recurring "two surfaces" distinction (S2). Each layer degrades
independently: a failure at L4 does not imply a failure at L3, so a reviewer
should assess each layer on its own, not treat the model as a monolith.
Honest ceiling — guidance, not deployment. This is the most important fact about S17. A reference informs; it secures nothing by itself. Posture throughout is stated as "engages as tested," never "verified" or "secure," and the concrete tools the model names are not deployed by agentteams — the operator deploys them, or does not. Treating the presence of this reference in a generated tree as evidence that the deployed system is hardened is exactly the category error the ceiling exists to close. For a threat modeler: this document is a checklist of where to look, not a claim that the looking has been done.
Source.
agentteams/templates/universal/security-infrastructure-layers.reference.template.md:31-70,119-130.
Full treatment: Edition R, S17.
Sandbox emission and privilege profiles ✅ (Linux bwrap deny-tested) / ⚙ (macOS Seatbelt UNVERIFIED, Windows design-only) {#S18}¶
The adversary: an in-repo agent (S1) that would use OS-level reach — writing outside its scope, or reading a credential directory — to escalate. The control is a requested OS sandbox selected by a privilege profile:
| Profile | OS boundary | Emits |
|---|---|---|
cooperative |
None — the opt-out | No sandbox request |
confined |
Requested — today's default | A sandbox request token |
exclusive |
Requested + read-exclusion | A sandbox request token and denyRead |
An unknown profile fails closed — it raises rather than silently
downgrading. denyWrite of exact control-plane paths (agent-privilege.json,
the constitutional-gate.py hook) applies deny-over-allow so an in-sandbox agent
cannot rewrite the enforcement plane governing it; exclusive adds denyRead of
credential directories (SSH/AWS/etc.) — outbound hardening of files, not env
vars, which stops the confined agent reading your tree, not others reading it.
The reviewer-critical fact: agentteams emits, the harness enforces. For
Claude it injects an inert sandbox example into settings.hooks.example.json
(the operator must merge it; agentteams never writes a live
.claude/settings.json); actual enforcement is Claude Code's own OS sandbox
(macOS Seatbelt / Linux bubblewrap). For Goose, on macOS only, it emits an
Apple Seatbelt profile; Goose has no native Linux/Windows OS sandbox.
Honest ceilings (all binding).
- Emitted blocks are inert until wired — an un-merged example enforces nothing.
- The empirically-verified path is Linux — a live-kernel bwrap deny test (the framework-neutral
sandbox/confine-run.shlauncher; see the Sandboxing Guide). macOS Seatbelt is UNVERIFIED (no on-mac deny test). - Claude Code's native Linux bubblewrap arm has the open D-3 fragility (distinct
from the verified launcher above): a
denyWriteof a non-existent path failsbwrapinit and blocks all Bash — a fail-shut availability defect (not a hole) that remains open in the remediation log. ItsdenyRead(incl.~/) and product arm are verified on Linux only under the AppArmor precondition in the next item. - Claude Code's native sandbox fails OPEN without
failIfUnavailable(fixed 2026-09-30). Measured on Linux withsocatmissing: the old block (enabled: true,allowUnsandboxedCommands: false) let Claude Code print "Sandbox disabled … Commands will run WITHOUT sandboxing" and run unconfined. The block now emitsfailIfUnavailable: trueon macOS/Linux targets (omitted on native Windows, where the block is advisory), so Claude Code refuses to start instead. Linux needsbwrapandsocat(scripts/install-sandbox-deps.sh); Ubuntu'skernel.apparmor_restrict_unprivileged_userns=1makes every sandboxed command fail closed at the seccomp step. That native arm is now 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/bwrapprofile, which disables Ubuntu'sbwrap-userns-restricthost-wide; without it sandboxed commands fail closed. Operators who merged an earlier block: re-run--updateand re-merge thesandboxblock. - Native Windows has no emitted enforcement — design-only.
- On a host where confinement cannot be enforced, the interactive path fails
closed (
PrivilegeConfinementError) unless the operator passes--allow-unenforced-confinement.
Because agentteams only emits configuration the operator must merge, all of this
stays dormant out of the box until it is wired in — even under the default
confined profile, which emits the boundary but cannot enforce it unmerged.
Full profile mechanics and line numbers: Edition R, S18.
Source. agentteams/host_features.py:134-261;
agentteams/frameworks/_sandbox_emit.py:25-208;
agentteams/frameworks/_goose_sandbox_emit.py:1-222;
references/agentteams-remediation-log.csv (D-3);
agentteams/cli/artifacts.py:321-411.
The constitutional-gate hook ✅ (fail-closed under confined/exclusive; fail-open by default) {#S19}¶
The adversary: an agent's own Bash/Write tool calls, which never pass
through the CLI entry points the gates (Part IV) guard. The hook is the runtime
counterpart that catches them. It is a PreToolUse hook the harness runs before
each tool call, enforcing C-4/C-5 on agent-initiated actions. Because the
harness runs it — not the agent — it is "not merely another file the agents can
edit on their way past it."
Split verdict: deny for deterministic high-severity write findings
(credentials, PII, injected override text — C-4); this deny is the runtime
expression of a HALT. ask for procedural Bash Mandatory-Review triggers
(C-5): privilege escalation, piping remote content into a shell, recursive
force-removal, persistence mechanisms, package installs, history-rewriting
force-push, and data-carrying outbound sends (S-9 criterion 6: uploads, raw
sockets, remote copies, gists, new git remotes) — requiring operator confirmation rather than an outright deny. Before
scanning a write it verifies the scanner against the integrity manifest (S22);
a tampered scanner degrades to ask, not a silent allow.
Default posture — fail-open, flipped fail-closed only by an explicit profile.
The hook is fail-open by default: if it cannot run to completion, the action
proceeds. agentteams flips it fail-closed only for an explicit confined/exclusive
in the brief — the defaulted confined profile does not trigger the flip, so
the default posture stays fail-open; --allow-fallback-fail-open restores open under
those profiles. Same emit-but-inert posture as S18.
Decision log (opt-in) — evidence, not a boundary. With AGENTTEAMS_GATE_LOG
set to an absolute path outside the project and outside any git work tree, the hook
appends one JSON line per invocation: tool_use_id, tool name, decision
(pass/ask/deny/crash-open/crash-deny), a fixed reason_id, scanner
categories and line numbers, what it checked, the hash of the installed hook, and a
prev_sha chain. It never records written content, the free-text reason or an
exception message. AGENTTEAMS_GATE_LOG_KEY adds a keyed input_hmac of the
canonical tool_input. An unsafe path (relative, inside the project or a work tree,
a symlink, not a regular file) is refused with a stderr notice; a failure to log
never changes the decision. Read it conservatively: a missing line means no evidence,
never a pass, and checked=none is not evidence the call was inspected. Changing the
variable in a settings env block is a C-3 change; in an evaluation, keep the log
path on the sandbox denyWrite list and under permissions.deny for Write/Edit.
Honest ceiling (E4) — cost, not impossibility. This is the residual risk that
sits at the center of the whole stack's honesty. The hook does not escape a
determined multi-file tamper: an attacker who can edit the scanner can also edit
the integrity manifest and this hook file. What the composed layers do is raise
the cost from one edit to three, each visible in git — a single silent change
becomes a multi-step, recorded, diffable act. The hook file is itself one of the
denyWrite-protected paths (S18), which adds the (Linux-deny-tested) sandbox as a
further obstacle to that third edit. The residual ceiling stands: composed layers
raise cost and make tampering evident; they do not eliminate it. Full mechanism:
Edition R, S19.
Source.
agentteams/templates/universal/hooks/constitutional-gate.py.
Sources for Part VI.
agentteams/templates/universal/security-infrastructure-layers.reference.template.md;
agentteams/host_features.py; agentteams/frameworks/_sandbox_emit.py;
agentteams/frameworks/_goose_sandbox_emit.py;
references/agentteams-remediation-log.csv (D-3);
agentteams/cli/artifacts.py;
agentteams/templates/universal/hooks/constitutional-gate.py. Line-precise
provenance: SOURCES.md (S27).