High-security, low-overhead sandbox wrapper for running AI agents in a disposable overlay of real project directories. It creates per-project overlayfs mounts, isolates the environment with bubblewrap, and records each session.
Supports the standard directories of claude code, codex, opencode and pi out of the box. Which of them get mounted lives in ~/.agent_sandboxes/config, written on first run — no need to edit the script.
- OverlayFS-based "undo": agent changes land in an upper layer, not your source tree
- Multi-project support: wrap multiple projects simultaneously with isolated overlays
- Persistent bash history: per-project command history preserved across sessions
- Bubblewrap isolation with a minimal filesystem view
- Optional SSH scoping via a synthetic, per-host config
- Session recording to timestamped logs
- Flexible sync operations: apply changes (
--sync-out), discard changes (--sync-in), or check diffs - Read-only mounts for common system and user tooling
- Mount defaults in a config file (
~/.agent_sandboxes/config), generated on first run - Maps conda and nvm environments from outside to within the container
This script targets Linux.
Required:
bubblewrap(bwrap)fuse-overlayfsscript(fromutil-linux)
Optional:
rsync(for--sync-out,--check-diff)ssh(for--allow-ssh)
No install step is required. Keep agentwrap.sh somewhere on your PATH or call it directly.
agentwrap [OPTIONS] /path/to/project1 [/path/to/project2 ...] [-- command ...]Important: Options must come before project paths.
If no command is provided, an interactive bash shell starts inside the sandbox. Use -- to separate project paths from the command if the command itself might start with / or look like a path.
Note: All options must come before project paths.
--mount-home: Mount your entire home directory read-only inside the sandbox--mount-ro <path>: Add an extra read-only mount (can be used multiple times)--mount-to-ro <src> <dest>: Add an extra read-only mount fromsrctodest(can be used multiple times)--mount-rw <src[:dest]>: Add an extra read-write mount; if:destis omitted, mounts to the same path inside (can be used multiple times)--mount-to-rw <src> <dest>: Add an extra read-write mount fromsrctodest(can be used multiple times)--no-mount <item>: Skip a default mount by name or path (can be used multiple times), e.g..geminior~/.claude
--allow-ssh <host>: Allow SSH only to the specified host (can be used multiple times for multiple hosts)
--allow-gpu: Pass NVIDIA GPU devices (and/sys, CUDA libraries) into the sandbox--allow-apptainer: Enable Apptainer (Singularity) container execution inside the sandbox--allow-flatpak: Pass through the per-user flatpak runtime dirs (/run/user/$UID/{.flatpak,.flatpak-cache,.flatpak-helper,bus,doc}), mount/sys, and setXDG_RUNTIME_DIR/DBUS_SESSION_BUS_ADDRESS
By default the sandbox inherits the full host environment (including any secrets exported in your shell); the sandbox only overrides a few variables like PATH and HOME.
--clearenv: Start from an empty environment instead (passed through to bwrap; built-in variables likePATH/HOMEare still set)--setenv <var> <value>: Set a variable inside the sandbox (bwrap syntax, can be used multiple times; applied last, so it overrides everything else). Tip: with--clearenv, add--setenv TERM "$TERM"for interactive TUIs.
--sync-out: Copy the merged sandbox view back into the real project(s) and exit (usesrsync)--sync-in: Discard all sandbox changes and reset to match the real project(s) (deletes overlay state)--check-diff: Show differences between the sandbox view and real project(s) (usesrsyncdry-run)--sync-exclude <path>: Exclude a path pattern from sync operations (can be used multiple times)
--unlock: Remove stale lock file(s) for the specified project(s) and exit--print-config: Print the built-in mount config template to stdout and exit--help: Show usage information
The default mounts under $HOME live in this file, not in the script. It is created from a built-in template on the first run and sourced as bash, so $HOME and other variables expand and conditionals work:
# agentwrap config v1
SANDBOX_RO_MOUNTS=( # toolchains and identity the agent may read
"$HOME/.nvm"
"$HOME/.miniconda3"
"$HOME/.local"
"$HOME/.gitconfig"
"$HOME/.ssh/known_hosts"
)
SANDBOX_RW_MOUNTS=( # agent state that has to survive the sandbox
"$HOME/.cache"
"$HOME/.claude"
"$HOME/.claude.json"
"$HOME/.codex"
"$HOME/.gemini"
"$HOME/.pi"
"$HOME/.local/share/opencode"
)
SANDBOX_NO_MOUNTS=() # entries to drop from the lists above- Entries are
SRCorSRC:DESTlike--mount-ro/--mount-rw. - A missing source is skipped with a warning instead of aborting the sandbox, so listing an agent you haven't installed is harmless. Command-line
--mount-ro/--mount-rwstill fail hard, since those are explicit requests. - System paths (
/usr,/bin,/lib,/lib64,/etc) and session state (bash history, ssh jail, ssh control sockets) stay built into the script. - Precedence is built-in → config → command line, and
bwraplets later binds win.--no-mountandSANDBOX_NO_MOUNTSapply to the first two tiers, never to explicit command-line mounts. - Run
agentwrap --print-configto reprint the current template, e.g. to diff your file against it after an update.
Changed in this version: these arrays used to extend a hardcoded list inside the script; they now define the $HOME tier. A config file written before this change lacks the # agentwrap config v1 marker on line 1, so agentwrap prints a one-time hint per run until you merge the template in (or just add the marker line if your file is already complete).
Start an interactive shell inside a project sandbox:
agentwrap ~/src/myprojectRun a single command:
agentwrap ~/src/myproject -- rg "TODO"Wrap multiple projects simultaneously (each gets its own overlay):
agentwrap ~/src/frontend ~/src/backend -- bashAll projects are accessible at their real paths inside the sandbox, with changes isolated to separate overlay layers.
Allow SSH to a host (scoped to its resolved config and key):
agentwrap --allow-ssh github.com ~/src/myprojectAdd read-only and read-write mounts:
agentwrap --mount-ro /opt/tools --mount-rw /data:/mnt/data ~/src/myprojectUse explicit destination flags:
agentwrap --mount-to-ro /opt/tools /mnt/tools --mount-to-rw /data /mnt/data ~/src/myprojectSkip selected default mounts:
agentwrap --no-mount .gemini --no-mount ~/.claude ~/src/myprojectCheck what changed in the sandbox:
agentwrap --check-diff ~/src/myprojectApply sandbox changes to the real project:
agentwrap --sync-out ~/src/myprojectDiscard all sandbox changes (reset overlay):
agentwrap --sync-in ~/src/myprojectApply changes but exclude certain paths:
agentwrap --sync-out --sync-exclude node_modules --sync-exclude .git ~/src/myprojectWorks with multiple projects too:
# Check diffs for both projects
agentwrap --check-diff ~/src/frontend ~/src/backend
# Apply changes to both
agentwrap --sync-out ~/src/frontend ~/src/backend
# Discard changes in both
agentwrap --sync-in ~/src/frontend ~/src/backend- User configuration:
~/.agent_sandboxes/holds the two files you edit:sandbox_profile- becomes~/.bashrcinside the sandboxconfig- the default mounts under$HOME(see above), generated on first run
- Per-project sandboxes: Each project gets its own sandbox under
~/.agent_sandboxes/<project>_<hash>containing:upper/- overlay layer where all modifications are storedwork/- fuse-overlayfs temporary directorymerged/- union view of the project (or symlink when not active)lock- prevents concurrent wrapping of the same project
- Session sandboxes: When wrapping multiple projects, a session sandbox is created at
~/.agent_sandboxes/session_<hash>containing:- Shared bash history (persists across sessions for this project combination)
- Session logs
- Entrypoint scripts
- OverlayFS mounting: Uses
fuse-overlayfsto create a writable union view without touching the real files - Bubblewrap isolation: Starts a container with controlled mounts, a minimal filesystem view, and network access
- Session recording: All terminal I/O is logged to
logs/session_<timestamp>.log - Lock-based safety: Per-project locks prevent accidentally wrapping the same folder alone and in combination
- The sandbox uses a "ghost home" (
--tmpfs $HOME) and selectively re-binds specific paths - DNS is copied from
/etc/resolv.confinto the sandbox with at least one public resolver - SSH access is opt-in; when enabled, only the selected host(s) and key(s) are visible
- Command history is persisted in the session sandbox and shared across sessions
- For single projects: history is project-specific
- For multi-project sessions: history is shared by that specific combination of projects
- Each project has its own lock file at
~/.agent_sandboxes/<project>_<hash>/lock - Double-mount protection: A project cannot be wrapped if it's already wrapped (alone or in any combination)
- Example: If
/project/ais wrapped alone, thenagentwrap /project/a /project/bwill fail - Remove stale locks with
--unlock
- When no sandbox is active, the
mergeddirectory is a symlink to the real project - This keeps external tools current when the sandbox isn't running
fuse-overlayfs: command not found: Installfuse-overlayfsand ensure it is on your PATHbwrap: command not found: Installbubblewrapand verifybwrapis availablefusermount: failed to unmount: Check for lingering processes in the sandbox and rerun after they exit- DNS issues inside the sandbox: Verify
/etc/resolv.confon the host and that outbound DNS is allowed - SSH failures with
--allow-ssh: Confirm the host is resolvable and your key is listed byssh -G <host> - Project already wrapped: Another sandbox is active for this project; close it first or use
--unlockto clear stale locks - Want to discard changes: Use
--sync-into delete the overlay and reset to the real project state - Options not recognized: Ensure all options come before project paths in the command line
This tool is a pragmatic isolation wrapper for local agent execution. It is not a hardened container runtime. Review and adapt the mounts and environment for your threat model.
MIT. See LICENSE.
This README was written by Codex from within the sandbox tool. :D