tree: 56a0e92a94af4491aafe5b328659a3bf3e14c6a2
  1. src/
  2. tests/
  3. agent-iso.sh
  4. check-tool-updates.sh
  5. claude-term-bg.sh
  6. git-global-post-checkout.sh
  7. git-hook-dispatcher.sh
  8. pinned-versions.toml
  9. prek-shim.sh
  10. pyproject.toml
  11. README.md
  12. sandbox-add-project-root.sh
  13. sandbox-bypass-warn.sh
  14. sandbox-error-hint.sh
  15. sandbox-status-line-rich.sh
  16. sandbox-status-line.sh
tools/agent-isolation/README.md

Table of Contents generated with DocToc

tools/agent-isolation/ — secure agent setup helpers

Capability: substrate:sandbox

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 --settings sandbox allowRead grant 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 (the kiro name normalises to its kiro-cli binary) with the same clean env; like OpenCode, it takes its filesystem isolation from the OS-level sandbox (no --settings grant).
  • agent-iso <cli> — launches any agentic CLI (codex, cursor, gemini, aider, …) with the same credential strip. The --settings injection is skipped for non-Claude CLIs, which take their filesystem isolation from the OS-level sandbox. The -w / --worktree flag 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 (including SSH_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 (see docs/adapters/add-a-harness.md).

All three paths 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 a git push from those harnesses is gated. An arbitrary CLI launched via agent-iso (codex, aider, …) receives the live SSH_AUTH_SOCK with nothing gating git 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.toml ships only the test harness, which runs under Python 3.11+ via uv). claude-term-bg.sh uses python3 / python for one heuristic and falls back to calm when absent.
  • CLIs: jq (required by check-tool-updates.sh and the status-line scripts), curl (the update check), git (status line / git hooks), and gh (optional — status-line PR title). The secure setup itself installs the pinned bubblewrap and socat (via apt-get) and @anthropic-ai/claude-code@latest (via npm — the agent runtime is intentionally unpinned; see pinned-versions.toml).
  • Credentials / auth: None for these helpers; the wrapped claude session authenticates on its own (and agent-iso.sh deliberately strips credential-shaped env vars).
  • Network: api.github.com and www.dest-unreach.org (the release checks in check-tool-updates.sh); the install step also reaches the apt and npm registries.

Files

FilePurpose
pinned-versions.tomlMachine-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 runtime installs at @latest so it always carries the newest permission-rule / sandbox / prompt-injection fixes.
check-tool-updates.shReads 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.shShell 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.shClaude 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.shClaude Code PostToolUse hook (Bash matcher). Scans the tool's stdout + stderr for the three known sandbox-shaped error signatures (SSH agent / Yubikey unreachable, loopback port-bind blocked, docker / podman socket denied) 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.shClaude Code statusLine helper. Renders <model> [sandbox] (green) or <model> [NO SANDBOX] (bold red) based on sandbox.enabled in the active settings — project settings.local.json first, then project settings.json, then user-scope, mirroring Claude Code's own precedence. Reflects in-session /sandbox toggles (which persist to project settings.local.json). Recommended user-scope.
sandbox-status-line-rich.shOpt-in richer alternative to sandbox-status-line.sh. Same sandbox-state detection, plus folder name (hash-coloured), git branch + dirty + ahead/behind, per-branch PR title (cached, gated by gh), and a yellow [sandbox-auto] tag for the autoAllowBashIfSandboxed setting. Wire one or the other into statusLine.command.
claude-term-bg.shOpt-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: Stopstop (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); Notificationnotify (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 + SessionStartreset (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.mdWaiting-for-input terminal tint.
sandbox-add-project-root.shAdds 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 #197allowRead: ["."] 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.shUniversal 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.mdPer-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.shUniversal, 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.mdWhole-user with the per-repo dispatcher.
prek-shim.shTransparent 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

Referenced by