sandboxing the Cursor agent with bubblewrap
- introduction
- quick start
- session continue
- what is bubblewrap
- how the wrapper works
- /usr/bin allowlist
- sandbox policy
- security limits
- checks and troubleshooting
Introduction
The Cursor CLI agent (cursor-agent) is useful for coding in a
terminal, but by default it runs with the same view of your machine as
you do. For everyday work I want it focused on the current project —
not casually reading .ssh, shell history, or unrelated files
in my home directory.
So I wrote a small bash wrapper,
cursor-agent-sandboxed,
that starts the agent inside
bubblewrap
(bwrap). The agent gets the project directory, an isolated
home under /home/agent, a selective
/usr/bin allowlist, and just enough of the system to run
tools and talk to the Cursor API. Longer notes live in
cursor-agent-sandboxed.md.
Requirements: Linux with user namespaces,
bwrap on the PATH (package
bubblewrap), cursor-agent under
/opt/cursor-agent, and host binaries
/usr/bin/node and /usr/bin/rg (ripgrep).
Quick start
Put the wrapper somewhere on your PATH, then:
cd /path/to/project
cursor-agent-sandboxed
That is roughly cursor-agent --continue inside the sandbox,
so the last chat session continues when possible.
cursor-agent-sandboxed --version
cursor-agent-sandboxed --list-models
cursor-agent-sandboxed -f "short question"
PROJECT_DIR=/path/to/project cursor-agent-sandboxed
CURSOR_AGENT_SANDBOX_BINS="python3 make" cursor-agent-sandboxed
Session continue
By default the wrapper prepends --continue so the last
session is loaded. Earlier versions used --resume, but that
flag optionally takes a chat id and would swallow the next argument
(for example a prompt) — hence --continue.
Auto-continue is skipped when you already pass:
--resume,--resume=*, or--continue- print/worktree flags:
-p,--print,-w,--worktree - meta flags:
--version,--help,--list-models, … - subcommands such as
login,logout,mcp,resume,help, …
Sessions need the real host store, so these directories are bind-mounted read/write into the sandbox home:
~/.cursor/chats→/home/agent/.cursor/chats~/.cursor/projects→/home/agent/.cursor/projects
Progress is therefore written back to the host. That is deliberate, and it widens the writable surface compared to a fully disposable sandbox.
What is bubblewrap?
Bubblewrap builds an environment from Linux namespaces and bind mounts.
You only see paths that were explicitly mounted
(--ro-bind, --bind, --tmpfs).
Anything else simply is not there inside the sandbox — stronger than
many “deny list” approaches for filesystem metadata.
This setup uses separate PID, IPC, and UTS namespaces. The network stack stays on the host so HTTPS to the Cursor API keeps working. There is no container image and typically no root. The whole policy lives in the wrapper script.
How the wrapper works
- Resolve the login home with
getent— never reuse an already isolated$HOME. - Create a host-side sandbox home:
~/.local/state/cursor-agent-sandbox/home. -
Seed a few files from the real home (copies):
~/.config/cursor/auth.json,~/.cursor/cli-config.json,~/.cursor/agent-cli-state.json, and~/.gitconfigif present. - Build allowlist binds for
/usr/bin/<name>(ALLOWED_BINS+CURSOR_AGENT_SANDBOX_BINS). - Add
--continuewhen appropriate. -
Exec
bwrap, which runs/opt/cursor-agent/cursor-agentdirectly — not viaPATH. The host symlink/usr/bin/cursor-agentis not mounted.
Inside the sandbox, HOME=/home/agent. Paths like
/home/<user>/.bashrc do not exist there.
Useful environment variables:
PROJECT_DIR— workspace to bind (default:$PWD)CURSOR_AGENT_REAL_HOME— source for seed files and chat bindsCURSOR_AGENT_SANDBOX_STATE— host root for the sandbox homeCURSOR_AGENT_SANDBOX_HOME—HOMEinside the sandbox (default/home/agent)CURSOR_AGENT_SANDBOX_BINDS— extra host paths, space-separated, each bind-mounted read/writeCURSOR_AGENT_SANDBOX_BINS— extra names from/usr/bin, space-separated (e.g.python3 make)
/usr/bin allowlist
Unlike an earlier version of the wrapper, /usr/bin is
not mounted as a whole. The script creates an empty
/usr/bin and bind-mounts only named files read-only. Names
missing on the host are skipped.
Built-in groups include:
- shell:
bash,sh,env - agent runtime:
node,rg - VCS / diff:
git,diff,patch - search basics:
ls,cat,grep,sed,awk,find, … - file ops:
mkdir,rm,cp,mv,chmod, …
Why node and rg must be listed:
/opt/cursor-agent/node is a symlink to
/usr/bin/node. Without that bind the agent dies with
/opt/cursor-agent/node: No such file or directory. The agent
also shells out to ripgrep; without rg you get
Could not find ripgrep (rg) binary.
Extra tools for one run:
CURSOR_AGENT_SANDBOX_BINS="python3 make" cursor-agent-sandboxed
This is not a kernel exec whitelist. Everything under
/usr/lib (including git helpers),
/opt/cursor-agent, and the project directory stays
reachable if the agent knows a path there.
Sandbox policy
Namespaces and runtime options include:
--die-with-parent— children die with the wrapper--unshare-pid/--unshare-ipc/--unshare-uts--proc /proc,--dev /dev, fresh--tmpfs /tmp--tmpfs /home— real home is not visible as a tree
Important mounts:
- sandbox home →
/home/agent(auth copy, caches) $PROJECT_DIRread/write- host
~/.cursor/chatsand~/.cursor/projectsfor sessions - empty
/usr/binplus per-binary allowlist binds - read-only
/usr/lib(and try: lib64, locale, CA share paths) - read-only
/opt/cursor-agent - usrmerge symlinks for
/bin,/lib,/lib64 - selected
/etc/…files for DNS, NSS, certificates, passwd/group
Not mounted (examples): the rest of /usr/bin,
/usr/src, /usr/include, the rest of the host
home, /var, other /opt trees.
Security limits
What this typically blocks or makes awkward:
- reading the rest of the home (
.ssh,.bashrc, …) - arbitrary tools from
/usr/bin(only the allowlist + optional extras) - writes outside the workspace, sandbox home,
/tmp, and the two Cursor session dirs - access to unmounted host paths
- orphan child processes after the wrapper exits
- finding
cursor-agentviaPATHinside the sandbox
What it does not give you:
- Network isolation — full host network stack (needed for the API)
- A tiny library set — all of
/usr/libremains available - Seccomp / kernel exec filtering — allowlist is mount-based only
- Disposable sessions — chats/projects stay on the host by design
- A VM or multi-tenant container — this is a namespace sandbox for a single trusted user
- Kernel / resource hardening — no defense against kernel bugs or CPU/RAM exhaustion
Checks and troubleshooting
Smoke tests:
cursor-agent-sandboxed --version
cursor-agent-sandboxed --list-models
Expect HOME=/home/agent, /usr/bin/node and
/usr/bin/rg present, tools like python3 absent
unless added via CURSOR_AGENT_SANDBOX_BINS, and no real
.bashrc / /usr/src.
Common issues:
bwrap … not found— installbubblewrap- namespace errors — user namespaces restricted by kernel/distro
/opt/cursor-agent/node: No such file or directory—nodemissing from the allowlistCould not find ripgrep (rg) binary— installrgon the host and keep it in the allowlist- agent fails to start —
/opt/cursor-agentor/usr/libnot bound; check usrmerge symlinks - TLS / certificate errors —
/etc/sslor/etc/ca-certificatesmissing Workspace Trust Required— confirm interactively, or use-f/--trust- auth failures — seed from missing
~/.config/cursor/auth.json - session finds nothing — empty
~/.cursor/chatsor bind failed - tool missing (
python3, …) — add viaCURSOR_AGENT_SANDBOX_BINSor editALLOWED_BINS
To tighten or loosen the policy, edit
ALLOWED_BINS and the bwrap argument list in
cursor-agent-sandboxed.
For a narrower network, use the host firewall — or
--unshare-net if you accept that the Cursor API will no
longer work without a proxy.
References:
bubblewrap on GitHub,
Arch package bubblewrap.