isolate

August 18, 2026 · View on GitHub

A tiny bash wrapper that runs AI coding agents (opencode, Claude, Cursor, Copilot, etc.) under filesystem isolation using bubblewrap.

Everything on your filesystem stays readable to the agent. Only a handful of paths — the current working directory, /tmp, some device nodes, and the agent's own config/auth directories — are writable.

This is deliberately a thin convenience script, not a hardened sandbox.

Install

# Fedora / RHEL
dnf install bubblewrap
# Debian / Ubuntu
apt install bubblewrap
# Arch
pacman -S bubblewrap

# drop `isolate` somewhere on your $PATH

macOS

isolate relies on bubblewrap, which uses Linux kernel namespaces and does not exist on macOS.

For macOS, Agent Safehouse provides similar filesystem isolation for AI coding agents using the built-in sandbox-exec / Seatbelt framework. It is a single bash script — no dependencies beyond macOS itself.

Usage

isolate opencode [agent-args...]
isolate claude
isolate cursor
isolate copilot

Run isolate with no arguments for the full list of supported agents and flags.

Set ISOLATE_DEBUG=1 (or --debug) to print the bwrap command instead of executing it.

Flags

FlagEffect
--allow-dockerExpose /run/docker.sock inside the sandbox
--allow-podmanExpose /run/podman/podman.sock inside the sandbox
--no-networkIsolated network namespace (loopback only)
--debugPrint bwrap command instead of executing

Environment variables:

VariableEffect
ISOLATE_DEBUGSame as --debug
ISOLATE_WRITABLESpace-separated extra writable paths
ISOLATE_MASK_ALLOWSpace-separated dirs to keep visible from the default mask list
ISOLATE_KUBE_CONTEXTSSpace-separated kubectl context names to proxy (default: current context)
ISOLATE_AWS_PROFILESSpace-separated AWS profile names to expose as read-only via a chained AssumeRole. Backed by an outside-sandbox credential minter holding the user's SSO session; the sandbox sees no static AWS credentials.
ISOLATE_AWS_SOURCE_PROFILESSO profile used as the root of the chained AssumeRole (required when ISOLATE_AWS_PROFILES is set).
ISOLATE_AWS_SHARED_SERVICES_ACCOUNTAWS account ID containing the intermediate source roles.
ISOLATE_AWS_SOURCE_ROLE_PATTERNPython .format() pattern for the intermediate role name, e.g. my-{name}-plan.
ISOLATE_AWS_TARGET_ROLEName of the read-only target role assumed in each account.

What it does

  • Creates a new mount namespace with the entire host filesystem mounted read-only.
  • Bind-mounts writable directories over the read-only base:
    • the current directory (pwd)
    • /tmp, /var/tmp
    • your language-runtime caches (.cache, .local, .npm, .bun)
    • the agent's own config and auth directories (.mcp_auth, .docker, .claude, .claude.json, .cursor, .config/cursor, .copilot, ...)
  • /run is replaced with an empty tmpfs, masking the Docker, podman, D-Bus, and libvirt sockets. Without a socket to connect to, those runtimes are genuinely inaccessible. A bash function also gives a friendly error if the agent types docker, podman, or virsh.
  • ~/.aws, ~/.kube, and ~/.ssh are masked with tmpfs. With ISOLATE_KUBE_CONTEXTS and/or ISOLATE_AWS_PROFILES set, isolate runs short-lived proxies or credential minters outside the sandbox and writes synthetic configs in /tmp so the agent gets read-only access without ever seeing real credentials. Kubernetes uses kubectl proxy with POST/PUT/PATCH/ DELETE rejected at the HTTP layer; AWS uses a chained STS AssumeRole into a read-only role in each account, with credentials refreshed on demand through a Unix socket. Inside the sandbox these denials surface as ordinary errors: kubectl writes return error: <method> against <path> is forbidden, AWS write API calls return An error occurred (AccessDenied) when calling the <Operation> operation: .... Both are the sandbox speaking, not a config bug.
  • SSH agent socket and DNS resolver are preserved through /run for git push/pull.
  • Mirrors a new /dev (device nodes) and /proc (isolated procfs).
  • Creates isolated PID, IPC, and session namespaces (--unshare-pid, --unshare-ipc, --new-session).
  • Strips a few environment variables that could leak tmux/systemd identity or enable a sandbox escape (DBUS_SESSION_BUS_ADDRESS, XDG_RUNTIME_DIR, TMUX, ...) and passes the rest through.
  • Shares the host network by default (--no-network for loopback-only).

Weaknesses — read this before you trust it

This is a helper, not a security boundary. It makes a mistake or a prompt-injection attack less damaging, not impossible.

  • Everything is readable. The agent can read any file you can, including ~/.ssh/id_*, ~/.aws/credentials, .env files, browser cookie stores, and private keys. Do not run agents on a machine where readable files contain secrets you wouldn't hand to the model.
  • Network is shared by default. Because the agent can read your whole filesystem and talk to the network, a compromised or prompt-injected agent can exfiltrate any readable data. There is no egress filtering. Use --no-network if egress matters.
  • Credential directories are writable. The agent's own auth directories (OAuth tokens, .claude.json, auth.json, etc.) are both readable and writable, so a malicious agent could rewrite or steal those tokens.
  • Environment passthrough is a denylist. Any environment variable not in the short strip-list (e.g. AWS_*, GITHUB_TOKEN, ANTHROPIC_API_KEY) is passed into the sandbox verbatim.
  • Not bulletproof. Kernel namespaces restrict filesystem and process and process access; they do not filter syscalls or provide a complete security boundary. A determined agent with code execution can look for ways to escape. Treat this as defense-in-depth, not a jail.

If you need real isolation, run the agent in a dedicated VM, container, or throwaway account. isolate is the convenient middle ground — better than unrestricted access, far weaker than a real sandbox.

Keyring-backed tools (e.g. gh)

Masking /run and stripping DBUS_SESSION_BUS_ADDRESS also hides the D-Bus session bus, so anything that keeps credentials in the system keyring (libsecret / secret-service) can't reach them inside the sandbox. GitHub CLI is the usual casualty: gh reports "The token in default is invalid" because its OAuth token lives in the keyring, not on disk.

The fix is to move the token to gh's file storage, which stays readable inside the sandbox — no D-Bus required. Run once, outside the sandbox:

gh auth token -h github.com \
  | gh auth login -h github.com --git-protocol ssh --with-token --insecure-storage

The token then lives in ~/.config/gh/hosts.yml (plaintext, mode 600). The same approach applies to other keyring-backed CLIs: use a file/env credential instead.

License

Released into the public domain (CC0). See LICENSE.