Sandbox
Legato can start each agent inside a sandbox made with bubblewrap. Inside, the agent sees its work, its agent tool, your toolchains and Legato’s sockets for it. It does not see your SSH keys, your logins, your password stores, or Legato’s database and configuration.
Use it when agents should not be able to reach your credentials, for example when you let them run without permission prompts. Together with the credential broker, it makes the approved way to use a key the only way.
Turn it on
Section titled “Turn it on”-
Install bubblewrap, which needs a Linux system that allows unprivileged user namespaces (most do):
Terminal window sudo dnf install bubblewrap # Fedorasudo apt install bubblewrap # Debian, Ubuntu -
Turn the sandbox on in
config.yaml:agents:sandbox:enabled: true -
Restart Legato.
From then on every agent Legato starts runs inside the sandbox. If bwrap is missing, or the sandbox cannot be built, the spawn fails with the reason. Legato never falls back to starting the agent outside.
What an agent sees
Section titled “What an agent sees”| Inside the sandbox | Access |
|---|---|
The system: /usr, /etc, /opt |
read-only |
The working directory, and a worktree’s main .git |
read and write. An agent that may only look (a find-out delegation) gets them read-only; a reviewer gets them beneath a throwaway layer: it can build and run tests, and what it writes is gone when it ends. That layer is on disk, and needs bubblewrap 0.10 or later; with an older one the reviewer’s checkout is read-only |
The agent tool’s own state, such as ~/.claude, ~/.codex or ~/.chimera, so sessions resume inside and out |
read and write |
| The agent tool’s installation | read-only |
Your toolchains, such as ~/.local/bin, mise, ~/go/bin, Rust and Bun |
read-only |
| Package and build caches for Go, npm, pnpm, Yarn, Bun, pip, uv, Cargo, Gradle, Maven and others | read and write |
| A home folder | an empty folder of the session’s own, on disk, with only the entries above in it |
/tmp |
a folder of the session’s own, on disk |
Legato’s sockets for the session: the SSH agent socket, the gh wrapper and the host broker |
read-only |
The legato binary |
read-only |
For an agent started as an engram: the engram binary, first on PATH, and engram’s store, so the agent’s own engram notes and memories are written |
binary read-only, store read and write |
Your known_hosts and the system’s ssh configuration |
read-only |
Agents never see:
- Legato’s configuration, database and data folder, and
~/.config/legato. ~/.ssh,~/.gnupg,~/.1password,~/.config/op,~/.config/gh,~/.gitconfig, git credential stores and~/.netrc.~/.aws,~/.azure,~/.kube,~/.docker, and the Docker and Podman sockets (with Docker in the sandbox on, an agent gets a filtered socket of its own instead).- Package registry logins:
~/.npmrc,~/.pypirc,~/.yarnrc, Gradle’sgradle.properties, Maven’ssettings.xml, Composer’sauth.json. - Password stores and keyrings, the session bus, and your runtime folder.
The environment loses the variables secrets come from, such as GITHUB_TOKEN, OP_SERVICE_ACCOUNT_TOKEN, cloud and registry tokens, and DOCKER_HOST. It gains your git name and email address, since ~/.gitconfig is not there.
The sandbox shares the host’s network and your user. It has its own process, IPC and hostname namespaces, and everything in it ends when the agent’s pane closes.
How agents still get their work done
Section titled “How agents still get their work done”- Legato commands. Commands an agent runs, such as
legato task noteorlegato credential exec, and Legato’s own tools and hooks reach the host Legato through a socket called the host broker. The host runs them for the agent’s own task, and nothing else. - SSH.
~/.sshstays hidden, so the agent has no keys of its own. A login works when ansshcredential covers the host: Legato’s SSH agent signs it. - GitHub. With a
githubcredential,gitfetches with read tokens even when the repository’soriginis an ssh address, and pushes go through Legato’s proxy.ghis always Legato’s wrapper inside the sandbox. - Azure. With an
azurecredential,azinside the sandbox is Legato’s wrapper: it signs in as the credential’s app before the first command that needs an account, once you allow it. See Azure. - Packages. A package already in a shared cache installs as it does outside. One that is not needs a registry that works without your login.
Add folders
Section titled “Add folders”Give every sandbox more paths with agents.sandbox.mounts. A path is absolute or starts with ~/, and access is read (the default) or write:
agents: sandbox: enabled: true mounts: - { path: ~/Documents/specs } - { path: ~/scratch, access: write }Legato refuses a mount inside a path it hides, such as ~/.ssh.
To give one agent a folder, use a resource instead. To give every agent that works in one repository a folder, use resources for a repository. A sandbox’s mounts are fixed when it starts, so adding a resource to a running sandboxed agent restarts it, resuming its conversation. The web UI asks first.
To leave out your toolchains and caches:
agents: sandbox: default_mounts: falseScratch space
Section titled “Scratch space”Each agent’s /tmp, its home folder and a reviewer’s throwaway layer are folders on disk, not in memory: <data_dir>/sandbox/<execution>/, readable only by you. A build cache an agent puts in /tmp uses disk space, not the memory the build needs. Legato removes the folder when the agent ends, however it ends, and sweeps the folders of agents that ended while it was not running. Folders of agents that are still running are never touched, and no agent sees another’s.
To keep scratch elsewhere, on a bigger disk say:
agents: sandbox: scratch_dir: /mnt/big/legato-scratchNothing limits how much an agent writes there, so a runaway agent can fill that disk.
Docker in the sandbox
Section titled “Docker in the sandbox”The sandbox hides your Docker and Podman sockets. A Docker socket is root on your machine when the daemon runs as root: any container could mount ~/.ssh, which would undo the sandbox. To let sandboxed agents run containers anyway, such as a project’s development stack with docker compose, turn on Legato’s filtered Docker socket:
agents: sandbox: enabled: true docker: enabled: true cap_add: [SYS_PTRACE] # capabilities containers may add; none by default publish: loopback # published ports bind 127.0.0.1 (the default), or anyRestart Legato. Each sandboxed agent then gets a Docker socket of its own, with DOCKER_HOST set to it. Legato passes what the agent asks to your Docker daemon (your DOCKER_HOST, else your Docker context, else /var/run/docker.sock; agents.sandbox.docker.host names another), but only what keeps within the agent’s sandbox:
- Its own things. Every container, network and volume the agent makes is labelled with its run (
legato.execution).docker ps,docker network ls,docker volume ls, events and prunes show it only its own. Inspecting, starting, stopping, removing,exec, logs,cpand joining a network work only on its own. Two agents can run stacks side by side without seeing each other’s, as long as their compose project names and published ports differ. - Only what its sandbox sees. A bind mount (
-v,--mount type=bind, a volume that binds a folder,--volumes-from) may name only a path the agent’s sandbox shows, with no more access: something it sees read-only is mounted read-only, and anything hidden is refused,~/.sshand the Docker socket among them. Links are resolved on the host first, so a link in the worktree to~/.sshis refused too. The sandbox’s/tmpand home are its own scratch folders, so a container mounting them gets those. Folders given as resources, including resources for a repository, can be mounted into the agent’s containers at the same access. - Nothing that is root on your machine. Refused:
--privileged, devices and GPUs,--cap-addbeyondcap_add, the host’s network, PID, IPC, UTS, user or cgroup namespace, another agent’s container’s namespace, security options that turn off seccomp, AppArmor or SELinux, a cgroup parent, sysctls that are not per container, other runtimes, andsystempaths=unconfined. A build on the host network is refused. Swarm, services, secrets, configs, plugins and nodes are refused. - Shared images. Pulling, building and listing images work as usual. An agent may remove only images it built.
- Ports. Published ports bind
127.0.0.1unlesspublishisany. Agents share your network, so they reach their own services there.
A refusal reads like any Docker error and starts with legato:, so the agent sees why:
docker: Error response from daemon: legato: privileged containers are not allowed: they are root on the hostWhen the agent ends, however it ends, Legato removes its containers, networks and volumes. Reloading an agent starts a new run, so its containers go too; start the stack again after a reload. Things left by an agent that ended while Legato was not running are removed when Legato next checks its agents. Another Legato’s containers are never touched.
Agents run without the sandbox, and your own shells, are unchanged: they use Docker exactly as before, with nothing filtered.
Private registries need a login, and ~/.docker stays hidden, so agents can pull only public images for now.
Run one agent outside
Section titled “Run one agent outside”You can start one agent without the sandbox:
- Web UI: tick Run without sandbox in the spawn dialog. It is unticked each time.
- TUI: press
ctrl+uin the spawn overlay. - API: send
"sandbox": falsetoPOST /api/agents/spawnorPOST /api/tasks/{id}/agent.
Such an agent is marked unsandboxed, in the web UI and in the TUI’s agents view. Swarm and workflow agents always run in the sandbox, and so does the Reeve, which cannot be started or moved out of it. The Reeve gets its own folder, ~/.local/state/legato/reeve, read-write. Its engram’s store is inside that folder, and its sandbox has the engram binary first on PATH, read-only, as for any agent started as an engram.
To move a running agent, press U in the TUI’s agents view, or in the web UI choose Reload without sandbox (or Reload in sandbox) in the agent’s More actions menu or its right-click menu in the sidebar. Legato restarts it on the other side of the sandbox, resuming its conversation. Reload agent keeps whichever side the agent is on. Legato tells the agent that the sandbox was added or removed, and the web UI shows that as a message from Legato.
Limits
Section titled “Limits”- What two sandboxes share is what both are given from your machine. All Claude Code agents share
~/.claude, and all Codex agents share~/.codex, with each other’s sessions in them. - A global hook of your agent tool that runs a program outside the mounts fails on every tool call. Add its folder with
agents.sandbox.mounts, or remove the hook. - Inside Legato’s sandbox, Legato drops Chimera’s own
--sandboxflags. Chimera’s sandbox is a container, and the container socket it needs would undo Legato’s. This is so even with Docker in the sandbox on. - With Docker in the sandbox, BuildKit’s own settings for a build sent over its session (
docker buildx,compose build) are applied by the Docker daemon, not by Legato. The daemon refuses insecure builds unless you allow them in its configuration; do not. - An agent that is running does not see a new SSH socket until it is reloaded, for example after another Legato on the machine takes over serving the credential channels.
- bubblewrap runs on Linux only.
For how the sandbox is built, see docs/ai/sandbox.md.
