# Shell jail and sandbox

> A kernel jail around every shell command an agent runs, and a throwaway VM for work that reads what a stranger wrote.

Rules decide whether a call runs. These two decide what a call can reach once it does. The shell jail wraps every shell command a Claude Code session runs on a Mac, so a command an agent was talked into still cannot read your keys or switch Golem off. The sandbox gives an agent somewhere else to do the riskiest work: reading a page from an issue, running a repository from a pull request, opening a download nobody vetted.

| | Shell jail | Sandbox |
|---|---|---|
| Runs | every shell command a Claude Code session runs | when an agent calls `sandbox_browse`, `sandbox_run` or `sandbox_open` |
| Boundary | macOS's own kernel sandbox, around the command and everything it starts | a throwaway Linux VM per task, on the same Mac |
| Keeps | your real toolchain, repositories, ssh, git signing, Docker, cloud CLIs | nothing of yours beyond what the agent hands over |
| Needs | macOS, Claude Code | Apple silicon, macOS 26 or later, Claude Code installed |
| Default | on | on, once installed |

## The shell jail

### What you do

Nothing, on a Mac. The installer puts Golem's shell into `~/.golem/bin` and points Claude Code at it by setting `CLAUDE_CODE_SHELL` in the `env` block of `~/.claude/settings.json` (to `golem-zsh`, or `golem-bash` if your login shell is bash). If you already set `CLAUDE_CODE_SHELL` to something else, Golem leaves it alone and the jail does not apply; `golem doctor` is the place to check. Restart Claude Code after installing so it picks the setting up.

### How it works

Before each command starts, Golem's shell asks the Golem agent on the machine for this session's policy, turns it into a macOS sandbox profile and applies it to the shell process itself. The command string is never parsed or rewritten. Because the jail is on the process, everything the command starts inherits it: an absolute `/bin/bash`, `nohup … &`, `python -c`, a script piped to `sh`.

```mermaid
flowchart TD
    A["Claude Code Bash tool"] --> S["Golem's shell"]
    S -- "session policy?" --> G["Golem agent"]
    G -- "clean or locked down, grants" --> S
    S --> Z["the command, inside the kernel sandbox"]
    Z -. "refusals" .-> G
    G --> L[("Signed record")]
```

### What a command cannot do

| | |
|---|---|
| Read | Private keys, judged by their contents rather than their names, so a key called `google_compute_engine` or a deploy key is covered. Shell history (`~/.zsh_history`, `~/.bash_history`, `~/.zsh_sessions`), `~/.netrc`, `~/.git-credentials`, GnuPG private keys and `~/.golem`. |
| Still read | The rest of `~/.ssh`: config, included files, known hosts and public keys, so ssh keeps working. |
| Write | Your shell profiles (`~/.zshrc`, `~/.bashrc`, `~/.profile` and the rest), `~/.gitconfig`, `~/Library/LaunchAgents`, `~/.ssh`, `~/.golem`, Claude Code's settings and hooks (including the repository's `.claude/settings.json`), and Cursor's hooks and MCP files. A hijacked command cannot switch Golem off or leave something behind to run at the next login. |
| See | Environment variables whose names look like secrets (containing `TOKEN`, `SECRET`, `PASSWORD`, `API_KEY`, `CREDENTIAL`, `ACCESS_KEY`, `COOKIE` and similar, or starting `GH_`, `GITHUB_`, `ANTHROPIC_`, `OPENAI_`) are stripped before the command starts. |

### What keeps working

Three grants, each on by default, keep normal development working. An owner or admin can turn each one off.

| Grant | What it allows | Turned off |
|---|---|---|
| SSH agent and keychain | `git push`, `gh`, commit signing, through the ssh agent and keychain. The key files stay unreadable either way. | The agent socket, the keychain and `gh`'s token file are denied. |
| Docker | The Docker socket (Docker Desktop, OrbStack, Rancher, Colima). | Denied. Worth considering: a container can mount the machine, which gets around the jail. |
| Cloud credentials | `~/.aws`, `~/.config/gcloud`, `~/.kube`, `~/.azure`, registry credentials in `~/.docker/config.json`, `~/.npmrc`, `~/.pypirc`, and their environment variables. | Denied, and the variables are stripped. |

> [!NOTE]
> While the Cloud credentials grant is on, cloud environment variables such as `AWS_SECRET_ACCESS_KEY` reach the command, because the CLIs need them. Turn the grant off, or add the variable to **Always strip these variables**, if you want them gone.

Two lists adjust the stripping: **Let these variables through** (for example `NPM_TOKEN`, or a prefix like `MYCO_*`) and **Always strip these variables**. Strip wins over let through.

### Lockdown

A rule can respond by locking a session down. From then until a fresh session starts, every shell command in it runs with:

- no network at all, localhost and DNS included;
- no grants: no ssh agent, keychain, Docker or cloud credentials, and the let-through list ignored;
- writes only inside the repository it is working in and its own temporary directory, with the repository's git hooks and git config still unwritable.

Lockdown only tightens. Nothing inside the session can lift it. `/safely` and a [quarantined machine](/response#quarantine-a-machine) also lock the shell down.

Three starter rules can lock a session down, and all three ship off, because people sometimes ask their agent to read the web or edit `~/.zshrc` on purpose:

- Lock down the shell after a web fetch.
- Lock down the shell after a prompt injection (the prompt-injection check scores a result at 0.98 or higher; the check itself must be on).
- Lock down the shell after a jail denial.

They are listed on the Sandbox settings under **Lock down a session**. See [Rules and CEL](/rules) for writing your own.

### Settings

In the console, **Sandbox**, then **Settings** at the top right, card **Shell commands**:

- **Jail agent shells** turns the jail on or off for every Mac in the workspace.
- The three grants, and the two variable lists.

To change them for one group of machines or one machine, set the **Sandbox: shell commands** section on a profile under Fleet. Only owners and admins can change these, and a change reaches machines in about 30 seconds.

### Cost, and if Golem stops

A jailed command starts about 30 ms later than it would bare. Golem's shell waits at most 250 ms for the session's policy. If the Golem agent does not answer in time, it uses the last policy it had for this session, then the machine's last known policy, then a baseline. In every case credential stores and persistence writes stay denied. It never runs a command unjailed because the Golem agent is down. The network is open in that fallback unless the session's cached policy was a lockdown.

### On the record

Each command is recorded, and each operation the kernel refuses is signed into the session as a `shield_denied` entry naming the command that tried it. A lockdown is signed as `shield_tainted`, with the rule that caused it. The console's **Sandbox** page shows, per day: coverage (sessions whose shell commands ran jailed), jailed commands, refusals, sessions locked down, commands run without the Golem agent, and commands that ran outside the jail. Refusals also appear in Events under the **Sandbox: shell** lens.

### What the jail does not cover

- It is macOS only, and Claude Code only. Linux and Windows are not supported yet. Other agents' shell commands are not jailed; their hooks and [rules](/rules) still apply.
- Claude Code's own Read, Write and Edit tools do not go through a shell, so the jail does not see them. Rules on those tools still apply.
- Commands you type in your own terminal are not jailed.
- A clean session has open network access. Only a lockdown takes it away.
- Programs that need setuid (rare in agent work) fail inside the jail.

## The sandbox

### What you do

On a Mac with Apple silicon and macOS 26 or later, the installer sets the sandbox up. It installs Apple's open-source `container` tool, checked against a pinned checksum and Apple's signature, and asks for your password once. Under MDM, running as root, it installs without asking. To skip it, set `GOLEM_NO_SANDBOX=1` when installing; to add it later, run:

```bash
golem install sandbox
```

On any other machine it reports that the sandbox is not supported and does nothing. The child agent is a Claude Code session, so Claude Code must be installed.

Golem also installs a `golem-sandbox` skill for Claude Code, Cursor, Codex, Copilot and Antigravity, which tells the agent when to reach for these tools: before fetching a URL from an issue or chat, before reading a repository or package it did not write, before opening a file from outside the project. You can ask for it directly too.

### The three tools

| Tool | Hand it | Network | Returns |
|---|---|---|---|
| `sandbox_browse` | a `url` and a `question` | the URL's host by default; `hosts` adds up to 20 more | an answer to the question |
| `sandbox_run` | a `task`, and a local `path` or an https `repo` to work on | none by default; `hosts` opens named hosts | an answer, and a patch for the agent to review before applying |
| `sandbox_open` | up to 20 file `paths` and a `question` | none by default | what it found |

Every result also carries a `deviations` list: each thing the child tried that it was not allowed to. An `egress_denied` entry names the method, host and port and why it was refused, for example `host_not_in_lockdown`. Other kinds are `browse_denied`, `guard_blocked` (the child tried a tool it does not have) and `token_cap`. The answer is marked as written by an agent that read untrusted content.

### How it is isolated

- The pages it loads, commands it runs and files it reads run in a fresh Linux VM with no network of its own, a read-only root, no capabilities and an unprivileged user. Its one way out is Golem's egress proxy, which allows only the hosts named and ports 80 and 443, and refuses private addresses.
- No credentials. Files go in read-only, and Golem refuses to hand over your home directory or a credentials directory. A repository is cloned inside.
- The child agent is fenced to the sandbox's own tools: no Bash, file, web fetch or search tools of its own. It runs on your Claude Code sign-in, capped at 200,000 tokens per task.
- Each VM is discarded when its work is done.

Isolation does not detect an injection. It limits what one can reach. So the child is watched like any other agent: its calls and every network request its VM makes are signed into the same record, linked to the session that handed the work over.

### Limits

| | |
|---|---|
| `sandbox_open` | 20 files, 100 MB each, 500 MB in all |
| A command inside `sandbox_run` | 2 minutes by default, 10 at most |
| A whole task | 5 minutes by default for browse and open (10 at most for open), 10 for run (30 at most) |
| A returned patch | about 100 KB |

### Settings

The sandbox is one switch for the workspace: **Sandbox**, then **Settings**, card **Sandbox**. It is on by default. Turned off, the three tools disappear from every agent's tool list.
