Agent isolation / layered sandbox
September 19, 2026 · View on GitHub
title: Agent isolation / layered sandbox status: stable kind: feature mode: infra source: > MISSION.md § Privacy, security and supply-chain integrity ("Clean- environment wrapper", "Layered sandbox by default", "Pinned, reviewed, signed dependencies"). Implemented in tools/agent-isolation/, tools/agent-guard/, tools/permission-audit/, tools/egress-gateway/, the setup-isolated-setup-* skills, and .claude/settings.json. acceptance:
- The reference setup uses an OS-level sandbox with default-deny filesystem reads and network egress; runtime-specific exceptions are documented in the adapter and this spec.
- Credential-shaped env vars are stripped before the agent execs.
- State-mutating shell calls (git push and every gh write subcommand, each listed explicitly) require a confirmation prompt; read-only gh subcommands do not; secrets/cred files are deny-read.
Agent isolation / layered sandbox
What it does
Runs reference agent invocations inside a layered sandbox so that even a successful prompt injection cannot read credentials or reach a non-allowed host. The fallback when prompt engineering fails is the OS saying "no".
Gemini CLI uses tool sandboxing rather than whole-process isolation.
Its native file tools restrict reads to allowed directories, but the tested
Linux backend exposes host files broadly read-only to approved shell commands.
Native credential-path policy denies do not block equivalent shell reads.
Tool network restrictions do not cover the CLI, hooks, or MCP servers;
existing sandbox grants can widen the baseline. See docs/adapters/gemini.md.
Where it lives
tools/agent-isolation/— the harness (clean-env wrapper + sandbox profiles).tools/agent-isolation/gpg-touch-overlay.sh(+ the two window scripts) — the hardware-key touch overlay: a window on screen while a signing key or ssh authentication key with a touch policy blocks waiting for a touch. Two entry points to one watcher:arm/disarmas aPreToolUse/PostToolUseBashhook around the agent's git commands, andwrapas git's own signing program and ssh command (gpg.ssh.program/gpg.programthrough an argument-freegpg-touch-wrap-<program>symlink,core.sshCommand … wrap ssh) for the commits and pushes the operator makes by hand — no git hook type sits at the right moment for those. Never two windows for one signature: inside an agent session (CLAUDECODE=1) the wrapper only runs the program and the hook's watcher shows the window; outside one, a watcher already recorded in the pid file is left alone. The git the agent runs reads the same global config, so the wrapper's two files are asandbox.filesystem.allowReadgrant of their own (nothing wider under~/.claude/), or every sandboxed signed commit fails withcannot exec. Installed bysetup-isolated-setup-installStep K, checked bysetup-isolated-setup-verifycheck 10. Capability:substrate:sandbox.tools/agent-guard/— deterministic pre-execution guard dispatcher (stdlib-only). Wired as aPreToolUsehook (Claude Code) or atool.execute.beforeplugin (OpenCode), with a--geminiadapter for Gemini CLI'sBeforeToolevent (wired in the repository's.gemini/settings.json; registration for snapshot adopters); inspects every shell command before it runs and denies the ones that break a hard framework rule, independent of model memory. The guard decisions live in a single harness-agnosticdispatch()core so every wired harness enforces an identical rule set. Capability:substrate:action-guard.tools/permission-audit/— audits and atomically edits Claude Code'spermissions.allow[]entries in.claude/settings.jsonand.claude/settings.local.json. Backs the--apply-permission-auditflag of/magpie-setup verify(check 8d). Also handles OpenCodepermissionconfig viaaudit-opencode. Capability:substrate:sandbox.tools/egress-gateway/— local HTTP(S) forward proxy for egress control. Framework tools pointHTTPS_PROXY/HTTP_PROXYat it; the gateway rejects any connection to a host not on its allowlist before a socket is opened. Defence-in-depth per RFC-AI-0003: even a prompt-injection reaching for an arbitrary endpoint is blocked at the network layer. Capability:substrate:sandbox..claude/settings.json— thesandboxblock (filesystem allow/deny, networkallowedDomains,excludedCommands) andpermissions(deny/ask)..gemini/settings.jsonand.gemini/policies/magpie.toml— Magpie's Gemini profile: tool-sandboxing and an explicitly loaded User-tier approval policy. Scoped shell reads are allowed; other shell calls, native edits, and MCP calls ask; listed commands and credential paths deny. Credential-path denies name the canonicalgrep_search; the native probe verifies itssearch_file_contentalias, canonical policy names, and search/multi-file/web/resource argument schemas against the loaded runtime.google_web_search,web_fetch, andread_mcp_resourcerequire approval for each call in every mode, including auto-edit and Plan Mode; headless calls are refused. Web search and URL fetching run outside shell network isolation; native MCP resource reads need an explicit rule because the MCP server-tool wildcard does not match them.list_mcp_resourcesretains the built-in allow for cached resource discovery. The native probe executes resource listing against a synthetic cache, asserts that metadata reaches the model, and rejects access to an MCP client. Plan Mode permits the scoped reads and approved web/resource reads, and denies other shell calls, file edits, and MCP server tools; YOLO and remembered tool approvals are disabled.sandbox-lint --gemini .geminichecks the static profile, with opt-in pytest integration tests against native 0.59.0 APIs for settings, policies, headless refusal, and Linux enforcement. Every Gemini upgrade requires revalidating the native probe against that version; static CI checks alone do not establish effective policy precedence.- Skills:
setup-isolated-setup-install,-update,-verify,-doctor. The diagnostic side — the failure catalog indocs/setup/sandbox-troubleshooting.md, thesandbox-error-hint.shhook, the doctor's live probes and the verify checks — is specified insandbox-diagnostics.md. docs/setup/secure-agent-internals.md— the three-layer model.
Behaviour & contract
The reference model is four layers, layered:
- Clean environment — a wrapper strips the process env to a
project-declared whitelist before exec (no
$GH_TOKEN,$AWS_*,$ANTHROPIC_API_KEYleakage).AGENT_ISO_ALLOWexplicitly names additional variables required by runtime authentication or tooling. It replacesCLAUDE_ISO_ALLOWwhen set, including an empty value; the legacy name remains supported otherwise. Unlisted variables stay stripped and values are never printed by the wrapper. - Filesystem + network sandbox — Linux
bubblewrap+socatSNI proxy; macOSsandbox-exec. Default-deny reads outside the tree and egress to non-allowed hosts.sandbox.excludedCommandscarves out commands that need host auth the sandbox blocks —gh(OS keyring and, on macOS, Security.framework TLS verification); the blast radius is held by layers 3 (gh auth token/gh auth refreshdenied) and 4 (ghwrites gated byask). The exemption applies only to invocations made ofcd …/gh …parts; the shape rule and its failure signature are insandbox-diagnostics.md. - Tool permissions — the host's
permissions.denyblocks denied paths/binaries (Read(~/.ssh/**),Bash(curl *), …). - Forced confirmation —
permissions.askongit pushand, on everyghwrite subcommand, listed one by one (gh pr merge,gh issue close,gh release delete,gh api, …). Not on a catch-allBash(gh *): Claude Code evaluates deny, then ask, then allow, and a matching ask rule prompts even when a more specific allow rule also matches, so a catch-all would silently defeat the read-only allows (gh pr view,gh * list, …) and prompt on every read. A subcommand in neither list falls through to the mode's default (prompt in default mode, classifier in auto).
Pinned system tools (bubblewrap, socat, agent CLI) are aged through a
cooldown window; bumps are PRs, not silent updates.
Out of scope
- The human-in-the-loop confirmation itself (that is the modes' job); this area provides the OS-level enforcement underneath it.
- Editing
.claude/settings.json(it is in thedenylist).
Acceptance criteria
- Filesystem and network default-deny with explicit allow-lists in the reference setup; Gemini's different boundaries are documented above.
- The clean-env wrapper strips credential-shaped vars before exec.
git pushand everyghwrite subcommand are inpermissions.ask, the read-onlyghsubcommands are inallow, and no catch-allBash(gh *)sits inask; secret/cred files are inpermissions.deny.
Validation
uv run --project tools/agent-isolation --group dev pytest
uv run --directory tools/agent-guard --group dev pytest
uv run --project tools/permission-audit --group dev pytest
uv run --project tools/egress-gateway --group dev pytest
python3 -c "import json,sys; s=json.load(open('.claude/settings.json')); \
ask=s['permissions']['ask']; \
sys.exit(0 if any(a.startswith('Bash(git push') for a in ask) \
and 'Bash(gh pr merge *)' in ask and 'Bash(gh *)' not in ask else 1)"
Known gaps
stable; drift shows up when a new state-mutating command is added to a skill without a matchingaskrule — the plan pass flags it.tools/agent-guard/andtools/egress-gateway/are new additions since the last pilot cycle; end-to-end integration with a real adopter session has not yet been exercised.
Gemini setup lifecycle
The four setup-isolated-setup-* skills route Gemini requests to
docs/adapters/gemini.md and stop before the Claude-specific procedure.
The install route merges the workspace profile and a single guard registration
from the existing extension, snapshot, or framework checkout, preserving
unrelated settings and requiring review of conflicts.
Extension setup must not introduce a second snapshot installation.
Verification distinguishes static configuration from live enforcement;
update and doctor report drift or diagnoses without applying changes.
Generic setup reconciles installed profiles and removes their guard references
before uninstalling the source.