Give the agent
a cage,
not
your keys.

Unattended, not unguarded. One command drops Claude Code into a per-project Docker box with permission prompts off.

curl -fsSL https://cleat.sh/install | bash
Press I to copy.

MIT licensed macOS, Linux, WSL2 Read the script first

cleat
✔ Docker tuned for Cleat (24 GB VM, room for many parallel sessions)
✔ Image ready (cached)
✔ Container started
✔ Auth shared
✔ Claude launched
Container: cleat-checkout-16506774
Project: ~/projects/checkout → /workspace
Caps: git, ssh, env, hooks, gh
█
Claude Code v2.1.282
Fable 5.1 with high effort · Claude Max
/workspace
! ls ~/.ssh
ls: cannot access '/home/coder/.ssh': No such file or directory
! cat ~/.aws/credentials
cat: /home/coder/.aws/credentials: No such file or directory
● high · /effort
❯ █
bypass permissions on (shift+tab to cycle) · ← for agents

One command

Run it. It's already contained.

cleat
✔ Image ready (cached)
✔ Container started
✔ Auth shared
✔ Claude launched
Container: cleat-checkout-16506774
Project: ~/projects/checkout → /workspace
Caps: git, ssh, env, hooks, gh
█

Same power. Smaller blast radius.

The campfire version is that an agent bricked my Mac overnight. It didn't. A shell can't. The truth is scarier: running as me, it could read every key I own and wipe my work. So I built Cleat.

Same power

Capability Without Cleat With Cleat
Edit project files ✓ ✓
Clipboard to host ✓ bridged
Paste an image (ctrl+v) ✓ restored
Browser auth (OAuth) ✓ bridged
Install packages your system contained
Run any command your system contained
Test Docker apps your system ✓--cap docker, breaks isolation

Wired up for you

Capability Without Cleat With Cleat
Parallel agents on one repo copy it by hand ✓a copy each with --fork
Worker + scout subagents wire it by hand ✓one command cleat kit
Start the Docker daemon start it yourself started for you, or the exact command

Smaller blast radius

Capability Without Cleat With Cleat
Access other projects exposed not mounted
Modify your system exposed your OS and apps untouched
Your machine stays clean global installs pile up global installs stay in the box
Read ~/.ssh exposed ✓not mounted without --cap ssh
Claude config & approvals shared, corruptible per-project session and hooks
Reach the internet open open, no egress filter
Your Claude login shared in the box, that is how it runs
Safe to leave overnight risky yes

Full network reach is the real gap there, not an oversight. The login is in the box by design. See what is being built about it

Architecture

Your machine. Docker container. Clean boundary.

Your machine

~/.claude Auth shared across all containers
(auth, settings)
~/my-project Your project files, mounted read-write
(project files)
~/.gitconfig Capabilities group by behavior: mount (git, ssh, env, hooks, gh) shares auth. Sandbox (docker) mounts the host socket and breaks isolation. Guard (unsafe-rm) turns off Claude Code's delete-safety prompt inside the box.
(read-only, opt-in)

Docker container

Sandboxed by default. Full permissions inside. No host access unless you opt in via caps.

/home/coder/.claude

/workspace

caps:

mount:git, ssh, env, hooks, gh sandbox:docker (breaks isolation)

Claude Code (full permissions)

Can:read/write project, install packages, run cmds Cannot:touch host or other projects (by default)

Everything else is untouched.

Runs where you work.

Host
macOS on Apple Silicon and Intel, Linux, Windows via WSL2
Engine
Docker Desktop, OrbStack, Colima, Rancher Desktop, Lima, rootful and rootless Linux
Shell
bash 3.2 (the macOS default) through bash 5
Arch
arm64 and amd64, prebuilt images for both

Every push gates the full suite on Linux, on macOS under the real bash 3.2 and on arm64 Linux, plus real-Docker integration on Docker Engine, where the arm64 leg builds and runs the image natively. WSL2, Colima and rootless Docker are advisory legs that report without gating. The rest of the Engine row has no CI leg at all. The docker capability is engine-aware, so a caged agent reaches your daemon wherever it runs, rootless and Colima included.

Why not just write a Dockerfile? You could. But then you handle UID/GID mapping, clipboard bridging, browser auth, hook forwarding, host connectivity, session persistence, per-project container naming and a dozen edge cases. Cleat handles all of it in one command.

Features

One command. Everything wired up.

#

A newer Claude Code is an offer, never a surprise.

When a newer Claude Code ships, the next interactive start says so and asks before it touches the image. Answer no and the box starts on the build you already have. The check is throttled, skipped when there is no terminal and switched off for good with CLEAT_NO_CLAUDE_UPDATE_CHECK=1.

▸ New Claude Code available: 2.1.156 → 2.1.161 (latest)

Update the image before starting? (downloads the new build) [Y/n] y

✔ Claude Code upgraded (2.1.156 → 2.1.161)

#

Seven capabilities. That is the entire list.

A box gets your project directory and your Claude auth. Nothing else on your filesystem is mounted unless one of the first six is on. Each of those names the exact thing it opens. The seventh opens no door: unsafe-rm disarms Claude Code’s un-bypassable delete prompt inside the box, off by default and never grantable from a project file. Even inside ~/.claude the box gets empties: a read-only projects directory holding this project's session and no other, plus its own blank copy of twelve private Claude directories including hooks, sessions and tasks. Memory and CPU live in the same editor, with the ceilings sized from your real Docker VM.

cleat config. Arrow keys, space to toggle, enter to save. Global, or written into this project's .cleat. For one session only, skip the file: cleat --cap ssh --env GH_TOKEN start.

cleat config

Cleat config (capabilities + resources)

Scope: global

Capabilities (space toggles host access)

[✔] git Mount ~/.gitconfig (read-only) for commit identity [✔] ssh Mount ~/.ssh (read-only) and forward SSH agent [✔] env Load env vars from ~/.config/cleat/env and .cleat.env [✔] hooks Run the hooks in ~/.claude/settings.json on the host [✔] gh GitHub CLI auth (persists across rm/nuke/rebuild) [·] docker Host Docker socket (breaks sandbox) to test Docker apps [·] unsafe-rm Run rm without the delete-safety prompt (global/CLI only, disarms a guard)

Resources (←/→ changes the value)

▸ memory ‹ 16g › (per-box RAM ceiling)

cpus all (core limit; all = no cap)

note: over half the 24 GB Docker VM. Two boxes at this ceiling can oversubscribe it, so run one at a time.

#

A cloned repo can't grant itself your keys.

A project's .cleat file can request host access in its [caps] section. Cleat names what it asked for and waits. Default is deny. Approve once, per project. Edit the file later and it asks again, because the approval is bound to what you actually read. Take it back with cleat untrust. Say yes to docker and Cleat says so out loud.

▸ Project .cleat [caps] requests host access: git, docker

Trust this project's .cleat? [y/N]

! Docker socket mounted. Container can create host-level processes

#

Your toolchain goes in the box, not on your machine.

A .cleat can also declare [setup] commands that run once inside the box. Cleat counts them, previews the payload and asks. This is a separate approval from capabilities: saying yes to caps never runs a command.

▸ Project .cleat [setup] wants to run 2 command(s) in the box as coder (full network access)

sudo apt-get update

sudo apt-get install -y libicu72

Run this project's setup commands? [y/N] y

✔ Setup applied (2 command(s))

#

Text leaves the box. Only images come back.

Copy inside the box with clip, pbcopy, xclip or xsel and it lands on your host clipboard. Reading it back does not work, so Cleat writes the rule into every box's own ~/.claude/CLAUDE.md rather than letting an agent guess. One thing crosses the other way: ctrl+v pastes a screenshot into a caged Claude. The host checks the bytes itself before it hands anything over, so an image is the only thing that can arrive. CLEAT_NO_CLIPBOARD_IMAGE=1 turns that off.

git log -1 --format=%B | clip

# Clipboard

`xclip`, `pbcopy`, `xsel`, and `clip` all work for COPYING text to the host clipboard. Paste/read-back (`xclip -o`, `xsel -o`, `pbpaste`) is not supported. Do NOT try to verify clipboard contents after copying. The copy succeeded if the command exits 0. Do not run a second command to check.

#

One project. As many sandboxes as you need.

A project is not one box. cleat start az makes a second sandbox beside main, with its own capabilities, memory ceiling, kit and description. cleat status is the screen that shows all of them. Every verb that acts on a box takes its name the same way: cleat shell main opens bash in the container you name, on a box that is already running.

cleat status

Container:
cleat-checkout-16506774
Status:
● running
Auth:
● logged in (via ~/.claude)
Caps:
git, ssh, env, hooks, gh

Boxes:

● main running

● az stopped

cloud box

○ dev not created

#

Several agents on one repo. None of them in your tree.

A forked box copies the project into its own workspace and mounts the copy. The Project line reads not mounted, because it is not. Start as many as your machine holds. None of them can collide. Get the work back with cd "$(cleat fork path feat-a)".

cleat fork start feat-a

✔ Workspace copied

Container:
cleat-checkout-16506774-feat-a
Project:
~/projects/checkout (not mounted, this box works on a copy)
Fork:
~/.config/cleat/forks/cleat-checkout-16506774-feat-a → /workspace (copied just now)
#

It stops the boxes you walked away from.

Close a terminal and the box keeps running, still holding its memory ceiling. On the next interactive start Cleat stops the ones that are provably idle past a 30 minute grace, then says how much came back. A box with a live agent is never touched. Nor is one with a shell open on it. It runs only from an interactive start. If the process list cannot be read the box is left alone. CLEAT_NO_IDLE_SWEEP=1 turns it off.

▸ Stopped 2 idle sessions, freed 12 GB. A detached box keeps its memory ceiling until stopped.

#

A dead Docker daemon is not your problem.

Rebooted and forgot? Cleat starts Docker Desktop, OrbStack, Colima, rootless Docker or Docker Desktop on Windows, waits for it, then carries on with the command you typed. The wait is bounded: 90 seconds by default, retuned with CLEAT_AUTOSTART_TIMEOUT_SECS. A remote daemon is never popped open, it is named instead. If the daemon is up but you cannot reach its socket, Cleat says exactly that and hands you the command that fixes it. With no terminal, or with CLEAT_NO_AUTOSTART=1, nothing launches.

! Docker isn't running. Starting Docker Desktop...

✔ Docker ready

#

Cleat can install Docker. It shows you the command first.

On a machine with no Docker at all, the first cleat start says so and offers to install one. macOS gets a Homebrew menu with Docker Desktop, OrbStack or Colima. WSL2 gets winget. Linux downloads Docker's own script to a file, then runs that file under sudo, never a blind pipe. Every leg prints the exact command before running it and every leg defaults to no. Decline and you get the one-line command instead. No Homebrew on the Mac and Cleat stops with the three official installer links rather than installing Homebrew for you.

cleat start

Install Docker now? (via Homebrew)

1) Docker Desktop (the standard; free for personal use, paid for larger companies)

2) OrbStack (fast, mac-native; free for personal use, paid commercially)

3) Colima (open source, CLI-only, no GUI)

n) no

Choice [1/2/3/n] 1

$ brew install --cask docker

#

Disk trouble, before it bites.

How full the shared Docker store is and exactly what Cleat can reclaim before it fills. One store sits behind every container on this host. You never have to remember to run it. A daily check offers the prune. Past 95 percent with under 10 GB free, Cleat holds the launch on a keypress so the warning cannot scroll by.

Docker storage one shared store, used by every container on this host

▸Disk53 GB / 60 GB88% full

On disk (all projects)
size
reclaimable
Images
36 GB
21 GB
Containers
2.1 GB
0.4 GB
Volumes
1.3 GB
0.0 GB
Build cache (shared)
7.8 GB
5.4 GB

Cleat can reclaim

7.0 GB stale cleat images cleat prune
5.4 GB shared build cache cleat prune --cache (all projects)

Other projects hold the rest. Cleat never deletes them. See docker system df.

#

Remove the container. Keep the conversation.

Session files live on your host, not in the box. Delete the container outright and the next resume builds a fresh one, then picks the same Claude conversation back up.

cleat rm

✔ Removed cleat-checkout-16506774.

Sessions preserved. Run cleat resume to pick up where you left off.

cleat resume

▸ No container for this project. Creating fresh (sessions will resume from host)

✔ Session resumed

#

When a box dies, it tells you why.

Every box runs with --memory, --memory-swap pinned to the same value so swap is off, --pids-limit 4096 and --init. A runaway test suite hits its own ceiling instead of the machine. Cleat names the cause and hands you the fix.

! Out of memory. The box hit its memory ceiling (box limit: 4 GB) and the kernel killed the process.

Test runners are the usual cause: jest/vitest start one worker per CPU, and a box sees every host core. Pick one:

• More memory: add [resources] then memory = 8g to .cleat (or your global config)

• Cap CPU so runners self-limit: add cpus = 2 under [resources]

#

Claude Code cannot delete one conversation. Cleat can.

Claude Code keeps every conversation on disk and only purges a whole project or sweeps by age. Cleat lists them with what they really cost and deletes the one you point at. The trash keeps it for 30 days.

cleat session list, with sizes

cleat session rename <id> give it a name

cleat session rm <id> move to the trash

cleat session restore <id> bring one back

cleat session trash see the trash

#

Two Claude accounts, one command to switch. No browser.

Cleat keeps each Claude login under a name and pins a box to one. Conversations and settings stay the same on both. Hit a limit and switch: the live session restarts on the other account in the same conversation.

cleat account pick a login

cleat account <name> pin or switch

cleat account default back to ~/.claude

cleat account rm <name> move to the trash

cleat account restore <name> bring one back

#

Heavy work burns your rate limit far slower.

Kits are curated Claude pre-configurations, one per box. The flagship asks you to run the session on Fable 5.1, then routes worker and scout subagents to Sonnet 5 in their own context windows. The kit never changes your session model. That knob stays yours.

cleat kit plan-big-execute-small

Enable kit plan-big-execute-small for box main?

Run your session on Fable 5.1: it plans and reviews while worker and scout subagents (Sonnet 5 by default) execute and explore, each in its own context window so the main session stays lean. Flagship judgment on the plan and every review, the mechanical bulk billed at the worker model's rate, so heavy work burns your rate limit far slower. Adapted from Anthropic's coordinator-pattern cookbook.

This kit adds, inside the box only:

agents
worker (model: sonnet), scout (model: sonnet, read-only by contract)
CLAUDE.md
a delegation policy section (appended, yours stays)
planner
your session's model, not changed by the kit

Your host ~/.claude and this repo are not touched.

Enable? [Y/n] y

Building with a team?

Something bigger is coming.

Get notified

Give the agent a cage, not your keys.

Start sandboxing Claude Code with one command.

Because you shouldn't need a Time Machine backup to use AI.

View on GitHub
curl -fsSL https://cleat.sh/install | bash

MIT licensed macOS, Linux, WSL2 Read the script first