Sandboxing Coding Agents: Permissions, Worktrees, Limits
A coding agent runs commands with your privileges unless you arrange otherwise. Sandboxing means deciding, before the run starts, which files the agent can change, which hosts it can reach, which secrets it can read, and how much it can spend. No single control covers all four, so the practical approach is to stack a few simple ones.
Start from least privilege
OWASP's list of risks for LLM applications includes Excessive Agency, and it names three root causes: excessive functionality, excessive permissions and excessive autonomy. Its mitigations read like a sandboxing plan: limit the extensions an agent can call "to only the minimum necessary," limit the permissions those extensions have on other systems, and use human approval for high-impact actions.
For a coding agent, that turns into four questions to answer before every run:
- Tools: which commands and actions does this task actually need?
- Paths: which directories should it read, and which should it write?
- Hosts: which network destinations does it need, if any?
- Budget: how many turns and how much money is this task worth?
Keep two threats in mind. The first is ordinary mistakes, such as deleting the wrong directory or force-pushing a branch. The second is manipulation: an agent reads READMEs, issues, logs and web pages, and any of them can contain text written to steer it. The agent does not need to be malicious to do damage. It only needs to be reachable.
Permission prompts and modes
Permission prompts are the first layer. The agent asks before a risky action and you approve or refuse it. Prompts work until there are too many of them, at which point people approve by reflex. Permission modes exist to trade prompts for rules.
Claude Code's permission modes show the range as of September 2026:
| Mode | Runs without asking | Intended for |
|---|---|---|
default (labeled Manual in the CLI) | Reads only | Reviewing every action yourself |
acceptEdits | Reads, file edits, common filesystem commands | Iterating on code you are reviewing |
dontAsk | Reads and pre-approved tools; the rest is denied | Locked-down CI and scripts |
bypassPermissions | Everything | Isolated containers and VMs only |
The docs list two more: plan, which blocks edits until you approve a plan, and auto, where a second model, a classifier, reviews most actions instead of you. The documentation is blunt about bypassPermissions: "Only use this mode in isolated environments like containers, VMs, or dev containers without internet access, where Claude Code cannot damage your host system."
For unattended runs, an exact allowlist is the tightest option. The docs give this shape for CI: reads and the listed actions run, and anything that would need a prompt is refused:
claude -p "run the test suite" --permission-mode dontAsk \
--allowedTools "Bash(npm test)" "Read"
Prefer allowlists to denylists. A deny rule such as Bash(git push *) blocks commands that match the pattern as written, but a shell offers many ways to spell the same action: the full path to git, a command wrapped in sh -c, a script that pushes. An allowlist fails closed. A denylist fails open.
Git worktrees isolate files, not privileges
Git's worktree command lets you "manage multiple working trees attached to the same repository." Each linked worktree is its own directory with its own checked-out branch, so an agent can edit freely without touching your working copy:
git worktree add -b agent/fix-login ../myapp-fix-login main
cd ../myapp-fix-login
# run the agent here and commit to the branch, then from anywhere in the repo:
git diff main...agent/fix-login
git worktree remove ../myapp-fix-login
Three details from the docs are worth knowing. A linked worktree shares everything with the main repository "except per-worktree files such as HEAD, index, etc.," which means branches and tags are shared. By default Git refuses to check out a branch that is already checked out in another worktree, which stops two workers from editing the same branch in place. And git worktree remove refuses to delete a worktree with uncommitted or untracked changes unless you add --force, so an agent's unreviewed work is not thrown away by accident.
The shared repository is also the limit of this tool. An agent in a worktree runs as your user, with your home directory, your SSH keys and your Git credentials. It can delete branches, rewrite refs and push. A worktree is a separate desk, not a locked room. Use it to keep changes apart and reviewable, and use the layers below for containment.
Containers and VMs
A container gives the agent its own filesystem view, process list and network stack while sharing the host's kernel. A virtual machine adds its own kernel, which makes a stronger boundary at the cost of more setup and memory. For most coding tasks, a container with a narrow mount is a sensible default:
docker run --rm -it \
--network none \
--user 1000:1000 \
-v "$PWD":/work -w /work \
agent-image:latest
Here agent-image stands for an image you build with the agent CLI and your toolchain installed. The command mounts only the current worktree, runs as a non-root user, and cuts off the network. Docker's documentation explains that with --network none, "only the loopback device is created". Never mount your home directory or the host's Docker socket into the container. The socket alone gives whatever runs inside control over the host's containers.
Some agent CLIs also ship an operating-system sandbox of their own. As of September 2026, Claude Code's sandboxed Bash tool uses the built-in Seatbelt framework on macOS and bubblewrap on Linux to restrict which paths and network domains commands can touch. It covers shell commands and their child processes, not the agent's own file-reading and editing tools, so treat it as one layer among several rather than a replacement for a container.
Network egress
Network access is how secrets leave and how outside instructions arrive, so default to none. Three patterns cover most work:
- No network. Build the container image with dependencies already installed, then run the agent offline.
- An allowlist. Permit your package registry and your Git host, and nothing else.
- Full access, inside a disposable VM. Only for tasks that genuinely need to browse, and never alongside credentials.
In Claude Code's sandbox, an allowlist and a set of blocked paths live in the same settings block:
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["~/.ssh", "~/.aws"]
},
"network": {
"allowedDomains": ["github.com", "*.npmjs.org"]
}
}
}
Keep secrets out of reach
Anything in the agent's environment variables or readable on its disk is available to every command it runs. So the rule is simple: secrets the task does not need should not be present at all.
- Do not launch agents from a shell that has production credentials exported.
- Block credential directories and
.envfiles in two places: the sandbox'sdenyReadlist above, which covers shell commands, and permission deny rules such asRead(~/.ssh/**), which cover the agent's own file tools. - When a task needs access, issue a short-lived token scoped to that task, for example one that can open a pull request but cannot push to your main branch.
- Be careful with repositories you did not write. Agent CLIs often read configuration from the project folder, and that configuration can define hooks or tool servers that run commands. Check what your agent loads from a repository before pointing it at unfamiliar code.
Time and cost limits
Every run needs a ceiling. As of September 2026, the Claude Code CLI reference lists two for headless runs. --max-turns limits the number of agentic turns and "exits with an error when the limit is reached." --max-budget-usd sets the maximum dollar amount to spend on API calls before stopping, and spend from subagents counts toward the cap. Both apply only in print mode:
claude -p --max-turns 30 --max-budget-usd 5.00 "Fix the failing date parser test"
A $5 cap per run sounds small until you run twenty agents at once, so set the number per task, not per habit. If your model provider supports spending limits on keys or workspaces, set one there too, because a flag only protects the runs that remember to pass it. Pair both with a wall-clock deadline enforced by whatever launches the agent.
A starting configuration
If you want one setup to begin with, use this:
- One worktree and one branch per task.
- A container running as a non-root user, with only that worktree mounted.
- No network, or an allowlist of your registry and Git host.
- No long-lived credentials in the environment or on disk.
- An exact tool allowlist, with bypass modes used only inside the container.
- A turn cap and a dollar cap on every run, plus a provider-level limit.
- A person reviews the diff before anything merges.
Loosen one layer at a time when a task proves it needs more, and write down why. A sandbox that grew through recorded exceptions is one you can still reason about. A sandbox that grew through reflexive approvals is not a sandbox anymore.