your ai coding agents ship with sandboxes. sandshell makes sure they're actually on.
Quick Start • Verbs • What it audits • What it applies • Drift • Threat model
sandshell detect • sandshell audit • sandshell apply • sandshell drift
Audit, apply, and drift-check the safety configs of every AI coding agent on your machine.
For each AI coding agent you have installed (Claude Code, Codex CLI, Gemini CLI), sandshell reads the safety config, reports what's risky, writes safe defaults, and tracks drift between sessions so quiet regressions don't slip through. It encodes the threat-model decisions you'd otherwise have to make by hand for each agent.
Sandshell is not a runtime sandbox. It works on the sandbox primitives your agent already ships with — Claude Code's native sandbox, Codex's Seatbelt policy, Gemini's
tools.sandbox. For runtime isolation of unvetted code, reach for a microVM tool like Dockersbx. Sandshell is the layer below: making sure the sandbox you do have is turned on, narrow, and stays that way.
sandshell detect # what AI agents and sandbox primitives do I have?
sandshell audit # which of them are risky right now?
sandshell apply # write safe defaults to every detected agentThen sandshell drift next session to see what regressed.
| Verb | What it does |
|---|---|
detect |
Report host inventory: OS, dependencies, native sandbox primitive, agents installed |
audit |
Report safety findings by severity. --summary for per-agent rollup, --json for machine-readable |
apply |
Write safe-default configs to detected agents (Claude Code, Codex CLI, Gemini CLI) |
drift |
Show what changed since the last apply / snapshot |
verify |
Re-run audit; exit 2 on findings ≥ medium (for CI / pre-commit) |
trail |
Inspect the Bash audit trail per Claude Code session (list, show, summary) |
prune-permissions |
Interactively prune entries from permissions.allow across all Claude scopes (pairs with the cc.permissions.review audit finding) |
install-agent |
One-time install of sandshell skill / instruction docs into detected agents (idempotent) |
uninstall |
Remove sandshell-managed configs across detected agents |
detect answers "what do I have?"; audit answers "is it safe?"; drift answers "did anything change since last time?". Use them together — detect once at install, audit whenever you want a safety review, drift whenever you want to spot config that's regressed since you last applied.
# 1. Clone the repo. ~/sandshell is a convenient default; sandshell computes
# paths from its own location, so anywhere on disk works.
git clone https://github.com/liwala/sandshell ~/sandshell
# 2. (Optional but recommended) put sandshell on $PATH so you can drop
# the ~/sandshell/bin/ prefix in the commands below.
echo 'export PATH="$HOME/sandshell/bin:$PATH"' >> ~/.zshrc # or ~/.bashrc
exec $SHELL
# 3. Inventory: what does sandshell see on your machine?
sandshell detect
# 4. Audit your current state — the "before" snapshot. Likely surfaces missing
# sandbox enablement, missing hooks, and per-agent configuration gaps.
sandshell audit
# 5. Install agent guidance (skill for Claude Code, instruction docs for the
# others). One-time setup; idempotent.
sandshell install-agent all
# 6. Apply safe-default configs to every detected agent (sandbox + hooks for
# Claude; safe TOML for Codex; safe JSON for Gemini).
sandshell apply
# 7. Confirm the issues from step 4 are resolved. Should be 0 actionable findings.
sandshell audit --summary
# Later, in a future session: see what's changed since you last applied.
sandshell driftIn CI / pre-commit, use verify (exits 2 on findings ≥ medium):
sandshell verify --json ┌──────────────────────────────────────────────────────┐
│ your machine │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Claude Code │ │ Codex CLI │ │ Gemini CLI │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ sandshell │ │
│ │ detect → audit → apply → drift → verify │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ sandbox on • bypass flags blocked • hooks wired │
│ audit trail logged • drift surfaced next session │
└──────────────────────────────────────────────────────┘
One taxonomy across three agents. Catches the silent-disable failure mode. Surfaces drift between sessions.
For project-specific safe defaults (committed to git, applied to your team when they clone the repo):
cd ~/myproject
sandshell apply project --profile=default # Claude Code
sandshell apply gemini project # Gemini CLI
git add .claude/settings.json .gemini/settings.json
git commit -m "Add sandshell safe defaults"Codex doesn't expose a project scope; its settings are user-level only.
sandshell audit — 2026-04-29T15:32:14Z
CRITICAL (1)
cc.sandbox.enabled
Claude Code sandbox is not enabled in any settings scope
fix: sandshell apply --profile=default
HIGH (1)
host.shell_alias_bypass
Alias 'claude' includes bypass flag '--dangerously-skip-permissions'
scope: ~/.zshrc:42
fix: Remove '--dangerously-skip-permissions' from the alias in ~/.zshrc
MEDIUM (2)
cc.hooks.pre_bash
Claude Code PreToolUse Bash guard hook is not configured
fix: sandshell apply
host.creds_in_shell_rc
Plaintext credential in shell rc: OPENAI_API_KEY
scope: ~/.zshrc:18
fix: Load via a secrets manager (op, aws-vault, vault, pass, chamber, infisical, gcloud secrets) at session start.
source: literal value. Materialized in ~/.zshrc, this credential ends up in every shell's environment.
3 actionable findings (severity >= medium).
Drift since 2026-04-23T11:08:02Z: +1 new / -0 resolved
+ cc.hooks.pre_bash (medium) Claude Code PreToolUse Bash guard hook is not configured
Per-agent adapters live in agents/<name>/audit.sh. Each reads the agent's real configuration files and emits findings. Coverage at a glance:
| Adapter | Surface |
|---|---|
host |
Cross-agent host hygiene. Bypass aliases (--dangerously-skip-permissions, --full-auto, --yolo), bypass env vars, plaintext creds in shell rc / .envrc, missing native sandbox primitive, non-git cwd, unknown repo provenance. |
claude |
Sandbox on and narrow; bypass paths denied. Sandbox enabled (catches the silent-disable trap), write/network scope, dangerouslyDisableSandbox deny entry, wildcard Bash permissions, curl-pipe-shell patterns, PreToolUse/PostToolUse hooks present, no project-level MCP auto-approve. MCP servers are cross-referenced against an opt-in allowlist at ~/.sandshell/known-mcps.json — silent until you create that file. |
codex |
Sandbox on, approvals required, no escape hatches. sandbox_mode != danger-full-access, approval_policy != never, no broad writable_roots, no trust_level = "trusted" for ~//, no network in workspace mode. PreToolUse + PostToolUse hooks present, [features] codex_hooks = true set (catches the silent-disable trap where hooks.json is ignored). |
gemini |
Sandbox on, folder trust on, YOLO and always-allow off. tools.sandbox configured, sandboxNetworkAccess = false, security.folderTrust.enabled, security.disableYoloMode, approval mode set, no wildcard entries in trustedFolders.json. |
The credential check classifies each export by injection source: $(op …), $(aws-vault …), $(vault …), $(pass …), $(gh auth token), $(gcloud secrets …), $(infisical …), and other recognized secrets-manager invocations stay silent; only literal values are flagged at medium.
Adapters self-skip when their agent isn't installed.
sandshell apply writes safe defaults for every detected agent. Each agent's config is independent — apply is idempotent, and you can target a single agent (apply codex) or all of them (apply / apply all).
| Agent | What apply writes |
macOS enforcement today |
|---|---|---|
| Claude Code | Native sandbox + Bash PreToolUse/PostToolUse hooks + skill | Filesystem ✓. Network ✗ (upstream #37970) |
| Codex CLI | ~/.codex/config.toml (sandbox_mode, network_access, approval_policy) + ~/.codex/hooks.json (PreToolUse + PostToolUse Bash) + [features] codex_hooks = true |
Filesystem ✓. Network ✓ (kernel-enforced via Seatbelt MAC) |
| Gemini CLI | ~/.gemini/settings.json: tools.sandbox (auto-detected: sandbox-exec on macOS, docker/podman on Linux based on what's installed), folder trust on, YOLO/always-allow off |
Filesystem ✓. Network ✗ under sandbox-exec (#20381); ✓ under tools.sandbox=docker |
On Linux, the upstream bugs above don't apply. Claude Code (bubblewrap) and Codex (bubblewrap + seccomp + Landlock) enforce both filesystem and network at the kernel. Gemini on Linux uses tools.sandbox=docker or podman (whichever is installed; sandbox-exec is macOS-only); the audit fires gemini.sandbox.linux_runtime_missing if neither runtime is available so the gap is visible rather than silent. Sandshell writes forward-correct config so users get the benefit automatically when the macOS fixes land.
Codex and Gemini are configured by a single file each — one place per agent for safety settings. Claude Code is layered, so apply writes three pieces:
- Native OS sandbox (Seatbelt on macOS, bubblewrap on Linux) — the main security boundary. Restricts writes to the project +
$TMPDIR, network to a profile-controlled allowlist, and denies--dangerouslyDisableSandbox. - PreToolUse + PostToolUse Bash hooks — narrow guards that block obvious sandbox-disable attempts and write a session JSONL audit trail to
~/.sandshell/audit/. - The Claude Code skill — instructs the agent to treat fetched content as untrusted input and surface suspicious instructions before acting.
# User scope (applies across all projects on this machine)
sandshell apply user --profile=default
# Project scope (just this repo; gets committed to git)
sandshell apply project --profile=python
# Strict mode also denies reads to ~/.ssh, ~/.aws, ~/.gnupg, etc.
sandshell apply user --profile=default --strictTo roll back:
sandshell uninstall userProfiles control which hosts Claude Code's native sandbox permits. (Codex and Gemini have their own per-agent network controls; profiles don't apply to them.)
| Profile | Hosts | Typical use |
|---|---|---|
default |
GitHub, npm, PyPI, Go module proxy | General projects |
node |
GitHub, npm, yarnpkg, jsdelivr, unpkg, nodejs.org | Node projects |
python |
GitHub, PyPI, conda/anaconda | Python projects |
The full host lists live in profiles/*.conf — one host per line, easy to fork. Each profile is independent (not a superset of default).
Claude Code and Codex CLI. apply wires each agent's PostToolUse Bash hook (which fires after every Bash command the agent runs) to append a one-line record per command into:
~/.sandshell/audit/<session-id>.jsonlEach record is a JSON object with ts, category (one of git, github_cli, sandshell, read_only, unclassified), cmd (truncated to 500 chars), and exit_code — easy to grep, easy to feed into a classifier later. Sessions from both Claude and Codex land in the same directory; the file name is the session ID, so they don't collide.
For Codex, the trail requires a feature flag in ~/.codex/config.toml:
[features]
codex_hooks = truesandshell apply codex flips this on automatically. Without the flag, Codex silently ignores ~/.codex/hooks.json — sandshell's audit catches this case (codex.hooks.feature_flag, high severity).
Helpers:
sandshell trail list # enumerate logged sessions, most recent first
sandshell trail show <session-id> # display every Bash command in a session
sandshell trail summary <session-id> # roll-up classification of a sessionThis is retrospective data, separate from sandshell audit (which is pre-flight config audit). Both are useful; they answer different questions.
Approved Bash/tool entries in Claude's permissions.allow accumulate fast —
every "always allow" choice during a session lands in settings.local.json.
The cc.permissions.review audit finding nudges you to revisit them; the
prune-permissions verb removes them without hand-editing four scoped
JSON files:
sandshell prune-permissions # interactive picker
sandshell prune-permissions --remove=1,3,5-7 --yes # by global index
sandshell prune-permissions --remove-matching=foobar --yes
sandshell prune-permissions --scope=project-local --dry-runEntries are enumerated across user, user-local, project, and
project-local scopes with a single global index. Removals are surgical
(other settings keys are preserved exactly) and print a unified diff per
file changed.
Every sandshell apply captures the post-apply audit state as a baseline at ~/.sandshell/baselines/current.json, plus a timestamped historical copy at ~/.sandshell/baselines/audit-<timestamp>.json. Subsequent sandshell audit runs compare against that baseline and report what's new or resolved:
$ sandshell drift
Drift since 2026-04-29T15:32:14Z: +1 new / -2 resolved
+ cc.permissions.wildcard_bash (high) Wildcard "Bash(*)" present in permissions.allow
- cc.sandbox.enabled (critical) Claude Code sandbox is not enabled — resolved
- cc.hooks.pre_bash (medium) Claude Code PreToolUse Bash guard hook is not configured — resolved
15 past snapshots in ~/.sandshell/baselines/ (oldest 2026-04-23).
This is the answer to "did anything change since I last applied?" — useful at session start, in pre-commit hooks, or whenever you want to spot quiet config regressions (a teammate's settings update, an agent self-modifying its own config, an out-of-band edit). Historical snapshots accumulate as a config-state audit trail that pairs with the Bash command audit trail above.
Configs shouldn't change often, so the snapshot directory stays small. Sandshell never prunes — rm ~/.sandshell/baselines/audit-*.json if you want to clean up old ones (current.json is the only one drift compares against).
sandshell drift # show only the diff (no full findings list)
sandshell audit # full findings + drift footer
sandshell audit --snapshot --no-drift # capture a baseline manually
sandshell audit --no-drift # suppress drift output (e.g. in CI)Agents already prompt before commands — Claude Code, Codex, Gemini, all of them. Sandshell sits one level up: it makes sure the config the prompts run inside is safe, narrow, and doesn't drift. Three things this solves that the prompts can't:
- One taxonomy across agents. Different UIs and bypasses across Claude / Codex / Gemini mean a user switching agents loses any safety habit. Audit reports them in one format.
- Catches the silent-disable failure mode. A
sandbox.enabled=falsesetting, a bypass alias, a--dangerously-skip-permissionsshell flag — none of these fire a runtime prompt; sandshell fires before the session. - Surfaces drift between sessions. When a setting regresses — teammate edit, out-of-band change, or the agent itself modifying its config — the next
auditflags it explicitly instead of letting it silently weaken your defaults.
| Threat | How |
|---|---|
| Misconfigured sandbox / silent disable | audit flags missing or disabled sandbox; apply writes correct config |
| Bypass flags persisted in shell aliases | audit parses shell rc files for --dangerously-skip-permissions, --full-auto, --yolo, etc. |
| Wildcard Bash permissions | audit flags Bash, Bash(*), and curl-pipe-shell patterns |
| Untrusted MCP servers | audit flags MCP servers not in your opt-in allowlist at ~/.sandshell/known-mcps.json — silent until you create the file |
Plaintext credentials in shell rc / .envrc |
audit parses each cred export and classifies the injection source — silent for known secret managers (op, aws-vault, vault, pass, gh auth token, etc.), medium for literal values; apply --strict adds read-deny for credential paths |
| Untracked host-side Bash activity | PostToolUse hook records every Bash command for retrospective review (Claude Code and Codex CLI; Gemini has no equivalent hook system) |
| Filesystem writes outside the repo | Native sandbox enforces filesystem bounds (Seatbelt on macOS, bubblewrap on Linux) |
| Threat | Why not |
|---|---|
| Network exfiltration on macOS (today, varies by agent) | Codex enforces network at kernel level via Seatbelt MAC — apply codex actually delivers. Claude Code has open bug #37970 — allowedDomains doesn't enforce for Bash subprocesses today. Gemini under sandbox-exec silently ignores sandboxNetworkAccess (architectural, not a bug); enforces under tools.sandbox = "docker". See KNOWN_ISSUES.md for full picture. |
| Untrusted code execution at runtime | Sandshell configures sandboxes; for unvetted dependencies or unknown repos, escalate to a microVM tool like Docker sbx |
| Supply-chain compromise | Sandshell doesn't detect malicious packages before install. Once they're running, the sandbox limits blast radius (network egress + filesystem reach), but that's containment, not prevention. |
| Prompt injection | Sandshell limits the blast radius of an injected agent (via the sandbox), not the injection itself |
| Real-time alerting / live monitoring | Sandshell is a config linter, not a daemon |
Current release: v0.2. See CHANGELOG.md for what's in it, SECURITY.md for the security model, NOTES.md for design notes, and CONTRIBUTING.md for tests and how to add an audit check.
- Real MCP audit signal. The MCP curation check is currently a hook for users who maintain their own
~/.sandshell/known-mcps.json(silent until you create the file). Sandshell deliberately doesn't ship a curated default list — being the trust authority for MCPs is not a position we want to occupy. The v0.3+ research direction is pulling real signal from external sources: GitHub Security Advisories by package name, postinstall-script analysis, freshly-published version detection. - Refine the PreToolUse Bash guard's matcher. It currently blocks any Bash command containing the literal trigger string, including inside quoted text in commit messages. Low-priority polish — workaround is to avoid the trigger string in shell-visible text.
- macOS or Linux
bash,python3,jq(Codex audit additionally requires Python 3.11+ fortomllib)- For the full Claude Code path: Claude Code with Bash
PreToolUse/PostToolUsehook support - For the Linux native sandbox:
bubblewrap(apt install bubblewrap)
sandshell detect reports the status of each requirement.
MIT

