tools/agent-isolation/
September 18, 2026 · View on GitHub
Capability: substrate:sandbox
Harness: agnostic
One exception to that tag:
sandbox-status-line.sh is Claude Code only.
Every other script here is harness-agnostic.
This directory ships the moving pieces the framework's
docs/setup/secure-agent-setup.md document
references. It is not a Python project (unlike the sibling tools
under tools/cve-tool-vulnogram/ and tools/gmail/oauth-draft/) — these are
plain shell scripts plus a TOML manifest of pinned upstream
versions.
The clean-environment launcher agent-iso.sh provides
harness-agnostic env isolation. It exposes four entry points, all sharing
the same env -i credential-strip core:
claude-iso— launches Claude Code; additionally injects a one-shot--settingssandboxallowReadgrant for the current repo (Claude-specific).opencode-iso— launches OpenCode with the same clean env; no settings grant (OpenCode takes its filesystem isolation from the OS-level sandbox).kiro-iso— launches Kiro CLI (thekironame normalises to itskiro-clibinary) with the same clean env; like OpenCode, it takes its filesystem isolation from the OS-level sandbox (no--settingsgrant).agent-iso <cli>— launches any agentic CLI (codex,cursor,gemini,aider, …) with the same credential strip. The--settingsinjection is skipped for non-Claude CLIs, which take their filesystem isolation from the OS-level sandbox. The-w/--worktreeflag is a Claude-only control flag and is stripped from the argv of non-Claude CLIs (it has no meaning for them). The Layer 0 env passthrough (includingSSH_AUTH_SOCK) is identical for every harness per RFC-AI-0002 § Layer 0; gating git push is a separate Layer 3 concern wired per-harness (seedocs/adapters/add-a-harness.md).
All entry points enforce layer 0 of the secure-agent posture regardless of
which harness drives the session. Harness-specific layers (the in-process
action guard, the permissions.ask confirmation list) are wired separately
per runtime — see docs/adapters/add-a-harness.md.
⚠️ Generic harnesses (
agent-iso <cli>) get Layer 0 only — no push gate. Claude and OpenCode ship a Layer 3 push gate (agent-guard /permissions.ask), so agit pushfrom those harnesses is gated. An arbitrary CLI launched viaagent-iso(codex,aider, …) receives the liveSSH_AUTH_SOCKwith nothing gatinggit push. The credential-strip posture is identical across harnesses, but the net protection is weaker: a runtime with no Layer 3 adapter is responsible for providing its own push gate before it is trusted with the agent socket.
Prerequisites
- Runtime: Bash + coreutils — this directory is plain shell scripts plus a TOML manifest, not a Python project (the
pyproject.tomlships only the test harness, which runs under Python 3.11+ viauv).claude-term-bg.shusespython3/pythonfor one heuristic and falls back to calm when absent. - CLIs:
jq(required bycheck-tool-updates.shand the status-line scripts),curl(the update check),git(status line / git hooks), andgh(optional — status-line PR title). The secure setup itself installs the pinnedbubblewrapandsocat(viaapt-get) and@anthropic-ai/claude-code@latest(vianpm— the agent harness is intentionally unpinned; seepinned-versions.toml). - Credentials / auth: None for these helpers; the wrapped runtime authenticates separately. For authentication requiring environment variables, use the explicit opt-in below.
- Network:
api.github.comandwww.dest-unreach.org(the release checks incheck-tool-updates.sh); the install step also reaches the apt and npm registries.
Files
| File | Purpose |
|---|---|
pinned-versions.toml | Machine-readable manifest of pinned upstream versions for the sandbox primitives bubblewrap and socat. Each entry carries a released date that satisfies the framework's 7-day cooldown convention. claude-code is deliberately not pinned — the agent harness installs at @latest so it always carries the newest permission-rule / sandbox / prompt-injection fixes. |
check-tool-updates.sh | Reads the manifest and reports upstream releases that are newer than the pin AND have themselves aged past the 7-day cooldown. Side-effect-free — no installs, no edits, no PRs. |
agent-iso.sh | Shell function to launch Claude Code with env -i and a tiny passthrough list, stripping every credential-shaped environment variable from the parent shell. The framework's "layer 0" of the secure setup. |
sandbox-bypass-warn.sh | Claude Code PreToolUse hook (Bash matcher). Prints a bold-red banner to stderr whenever the model invokes the Bash tool with dangerouslyDisableSandbox: true. Belt-and-braces visibility for the sandbox-bypass permission prompt. Recommended user-scope (~/.claude/settings.json) so it fires across every session on the host. |
sandbox-error-hint.sh | Claude Code PostToolUse hook (Bash matcher). Scans the tool's stdout + stderr for the four known sandbox-shaped error signatures (SSH agent / Yubikey unreachable, loopback port-bind blocked, docker / podman socket denied, temp files blocked under /tmp) and prints a [sandbox-hint] line pointing at the matching entry in docs/setup/sandbox-troubleshooting.md. Fail-open: any unexpected JSON shape exits silent. Recommended user-scope so the hint fires across every session. Complements setup-isolated-setup-doctor (the structured probe) by surfacing the catalog reference at the moment of failure, without the user having to remember the catalog exists. |
sandbox-status-line.sh | Claude Code statusLine helper, and the only one Magpie ships. Claude Code only — the one script in this directory that is not harness-agnostic: it is wired through Claude Code's statusLine setting, is fed Claude Code's statusLine payload on stdin, and reads Claude Code's sandbox.enabled schema. No other harness the framework supports has a status-line hook of that shape; Codex, Gemini, OpenCode and Kiro carry their sandbox posture in their own config and surface it, where they surface it at all, through their own UI. A harness that grows one gets its own helper — see docs/adapters/add-a-harness.md. Leads with the sandbox state — green [sandbox], yellow [sandbox-auto] when autoAllowBashIfSandboxed widens the blast radius, bold-red [NO SANDBOX] — read from sandbox.enabled in the active settings, project settings.local.json first, then project settings.json, then user-scope, mirroring Claude Code's own precedence, so in-session /sandbox toggles are reflected. Inside a linked git worktree the walk leads with the main checkout — that is where Claude Code scopes the project and where /sandbox persists the toggle, so it is the only file that can describe the session; reading <cwd> alone there falls through to user scope and paints a green [sandbox] on a session that has none. Layout-agnostic: worktrunk's <repo>.<branch>/ siblings, .claude/worktrees/<name>, and plain git worktree add all resolve the same way, and a bare repo's worktrees — which have no main checkout at all — fall through to user scope rather than reading the bare directory's parent. Then the context that tells sessions apart: hash-coloured folder (stable per repo and worktree), git branch + dirty + ahead/behind (local only, no network), the branch's PR number and title (cached, gated by gh, silent without it), and the model. Every segment degrades to silence when its input is missing. Install user-scope. |
claude-term-bg.sh | Opt-in quality-of-life helper (not a security control). Keeps a calm baseline background and tints it only when Claude genuinely wants you to act (never while working, and never when it merely finished a turn), so a window you've tabbed away from can't sit blocked unnoticed. Distinguishes "blocked on a decision" from "finished and idle" — which look identical at the Stop event — via three signals across six hooks: Stop → stop (heuristic — tints only if the final assistant message reads as a question/request; a completion stays calm; needs python3/python, else defaults calm); PreToolUse (matcher AskUserQuestion) → wait (exact — a structured question was posed); PostToolUse (matcher *) → reset (calm while working, and clears the tint the instant you approve a permission prompt or answer a question); Notification → notify (tints for permission prompts only — the plain idle ping is a no-op so it can't wipe a pending question's tint); and UserPromptSubmit + SessionStart → reset (you replied / fresh session clears any stale tint). Writes the OSC escape to the Claude pty discovered by walking the process tree (hooks have no controlling tty); the only deterministic reset is an explicit CLAUDE_RESET_BG colour via OSC 11 (iTerm2 ignores OSC 111). Colours overridable via CLAUDE_WAIT_BG / CLAUDE_RESET_BG. Tested on iTerm2 + macOS; fail-soft elsewhere. See docs/setup/secure-agent-setup.md → Waiting-for-input terminal tint. |
sandbox-add-project-root.sh | Adds the current adopter repo's project root (and, with --all-worktrees, every linked git worktree's working dir) as an explicit absolute path to sandbox.filesystem.allowRead and allowWrite in the project-local, gitignored <repo>/.claude/settings.local.json — one entry per worktree, each in that worktree's own settings file. Defensive against issue #197 — allowRead: ["."] does not in practice cover CWD because the harness pre-resolves the . literal away from the read side. Never modifies user-scope or committed project-scope. Idempotent, atomic, tolerant of missing prereqs. Invoked from setup-isolated-setup-install, /magpie-setup (adopt / upgrade / worktree-init), and the post-checkout git hook installed by /magpie-setup adopt. |
git-global-post-checkout.sh | Universal post-checkout git hook installed at ~/.claude/git-hooks/post-checkout when the operator picks the simple whole-user flavour in setup-isolated-setup-install. Activated by git config --global core.hooksPath ~/.claude/git-hooks/ so every git checkout / git clone / git worktree add across the host invokes it. Best-effort + idempotent + || true: invokes sandbox-add-project-root.sh for any worktree with a .claude/ directory. Trade-off documented in docs/setup/secure-agent-setup.md → Per-project vs whole-user scope: core.hooksPath shadows per-repo .git/hooks/* across every repo on the host. The dispatcher flavour (below) supersedes this file. |
git-hook-dispatcher.sh | Universal, basename-keyed git-hook dispatcher for the whole-user dispatcher flavour. Symlinked to every hook name under ~/.claude/git-hooks/. For each git operation it runs the framework's own logic for that hook type (the post-checkout sandbox-allowlist sync) and then chains through to the repo-local .git/hooks/<name> (resolved via git rev-parse --git-common-dir, worktree-safe; exec-ed with original argv + inherited stdin so a failing local hook still aborts the git op). Restores per-repo hooks (prek / pre-commit / husky / hand-written) under global core.hooksPath; a repo with no local hook is a clean no-op. See docs/setup/secure-agent-setup.md → Whole-user with the per-repo dispatcher. |
prek-shim.sh | Transparent prek front installed as ~/.claude/bin/prek (with ~/.claude/bin prepended to PATH) for the whole-user dispatcher flavour. Rewrites only prek install — injecting --git-dir "$(git rev-parse --git-common-dir)" unless the caller already passed --git-dir, asked for --help, or is outside a git work tree — so prek's shim lands in the repo-local .git/hooks/ where the dispatcher chains, instead of the shared core.hooksPath dir. Every other prek invocation passes through unchanged; no-op on hosts with no global core.hooksPath. |
Usage at a glance
# Initial install (read pinned-versions.toml for the bubblewrap/socat pins):
sudo apt-get install --no-install-recommends bubblewrap=0.11.1-* socat=1.8.1.1-*
# claude-code is unpinned — always install the latest for the newest security fixes:
npm install -g --no-save @anthropic-ai/claude-code@latest
# Source the wrapper into your shell:
source /path/to/magpie/tools/agent-isolation/agent-iso.sh
# Optional: make claude-iso the default `claude` (see docs/setup/secure-agent-setup.md
# for the trade-off — the alias also strips env in non-tracker sessions):
alias claude='claude-iso'
# Launch a session with no inherited credentials:
cd ~/code/<tracker>
claude-iso
# For any other harness — same credential strip, no Claude-specific settings:
agent-iso codex [codex-args]
agent-iso cursor [cursor-args]
agent-iso gemini [gemini-cli-args]
# Or directly without sourcing:
bash /path/to/magpie/tools/agent-isolation/agent-iso.sh agent-iso codex [codex-args]
# Periodically (or via /schedule weekly), check for upgrade candidates:
bash /path/to/magpie/tools/agent-isolation/check-tool-updates.sh
Explicit environment opt-in
AGENT_ISO_ALLOW names the additional variables the launcher may pass through, separated by spaces.
Values must already be set in the invoking shell or on the invocation line; naming a variable does not obtain a credential.
For example, with GH_TOKEN already set for a workflow that needs GitHub authentication:
AGENT_ISO_ALLOW=GH_TOKEN agent-iso <your-runtime-cli>
The same mechanism supports non-secret configuration such as a project ID or region.
Consult your runtime's adapter for required variables; the Gemini authentication recipes show one mapping.
Credentials intentionally passed through are available to the runtime; this is an explicit exception to environment stripping, not a separate secret store.
Store persistent credentials under the runtime's home-directory convention, never in a project .env file.
CLAUDE_ISO_ALLOW remains supported for existing installations and all harnesses.
When AGENT_ISO_ALLOW is set, it replaces that legacy list; an explicitly empty value permits no additional variables.
Setting KEY=value alone does not bypass the strip.
The wrapper does not print values, and rejects invalid variable names before launching the runtime.
Referenced by
../../docs/setup/secure-agent-setup.md— the user-facing setup document. Read that first.../../.claude/settings.json— the framework's own dogfooded secure config. Adopters scaffold their own version from the example block indocs/setup/secure-agent-setup.md.