Are you the author? Sign in to claim
Give the agent a cage, not your keys. One-command Docker sandbox for AI coding agents: full autonomous permissions, per-
Give the agent a cage, not your keys.
Unattended, not unguarded.
Run AI coding agents with full autonomous permissions, safely sandboxed in Docker.
One command. Per-project isolation. Your host stays untouched.
A real session, not a mockup: Claude Code hunts for keys, finds nothing, then rm -rf's the box's own OS. The host doesn't notice.
curl -fsSL https://cleat.sh/install | bash
cd ~/your-project && cleat
That's it. First run pulls the prebuilt image from GHCR (~30s), starts an isolated container for your project and drops you into Claude Code with full permissions, all sandboxed. If the prebuilt image is unavailable for your CLI version, it falls back to a local build (~2 min) automatically.
┌─────────────────────┐ ┌─────────────────────────────────┐
│ Your machine │ │ Docker container │
│ │ │ │
│ ~/my-project ───────────> │ /workspace │
│ ~/.claude ──────────────> │ /home/coder/.claude │
│ │ │ │
│ Everything else │ │ Claude Code runs free here: │
│ is untouched. │ │ install, build, delete, run │
│ │ │ anything. Fully sandboxed. │
└─────────────────────┘ └─────────────────────────────────┘
Stay updated: Watch → Custom → Releases on this repo and upgrade any time with cleat update.
Claude Code with --dangerously-skip-permissions is the fastest way to build software with AI. No confirmation dialogs, no permission prompts. Claude just does what you ask. But on your actual machine, that means:
Cleat gives you the best of both worlds:
| Without isolation | With Cleat | |
|---|---|---|
| Claude can edit project files | Yes | Yes |
| Claude can install packages | Yes (on your system) | Yes (in container) |
| Claude can run any command | Yes (on your system) | Yes (in container) |
| Claude can access other projects | Yes | No |
| Claude can modify your system | Yes | No |
| Claude can read ~/.ssh, credentials | Yes | Opt-in (via cleat config) |
| Safe to leave running overnight | No | Yes |
| File ownership issues | N/A | None (UID/GID mapped) |
| Copy to host clipboard | Yes | Yes (via clipboard bridge) |
cleat pulls (or builds) the image, starts a container and launches Claude Codepbcopy, xclip and xsel shims route to your host clipboard via a file bridge -- no X11 or special terminal features needed--cap git), SSH keys (--cap ssh), env var passthrough (--cap env), host hook execution (--cap hooks), GitHub CLI auth (--cap gh) and host Docker daemon for testing dockerized apps (--cap docker). All disabled by defaultcleat start pulls from ghcr.io/cleatdev/cleat (~30s) instead of building locally (~2-5 min), with automatic local-build fallbackopen and xdg-open inside the container forward URLs to your host browser (auth, OAuth, docs)host.docker.internal always available, user-defined hooks and MCP servers work out of the boxI was deep into vibe coding, letting Claude Code run with --dangerously-skip-permissions so it could ship without interrupting my flow. Kick off a task, step away, come back to working code. Multiple projects on my Mac, sometimes left running overnight through a big refactor.
The campfire version of the story is that one night it went rogue and bricked my Mac. The honest version is scarier, because it actually happens. The hardware was never in danger -- Apple sealed the OS so thoroughly that not even root can modify /System in place. Any Mac you can boot into Internet Recovery or DFU you can bring back. What an agent running with no gates can actually destroy is everything that isn't the OS. It runs as you.
So it installs packages system-wide and litters configs across your home directory until the machine you keep clean is quietly rotting. It reads ~/.ssh, ~/.aws, your .npmrc tokens and your .env files -- and it can commit or deploy those secrets straight to the internet. That one isn't hypothetical for us: one night an agent wired up a deploy and an API key rode along into a public static deployment. Nothing was "hacked" -- it was running as us, it had the key, it shipped it. Public keys get scraped and abused in minutes, not days. Providers rarely refund fraud on technically-valid requests. By the time we rotated it, roughly $10,000 was already gone.
And one bad glob ends the rest: developers have wiped entire home folders with a trailing ~/ on an rm -rf, where the shell expands the tilde to your whole home directory after the agent's own check passes. On a Mac's internal SSD, TRIM is on by default, so the freed blocks are discarded and the per-file encryption key is destroyed within seconds. No undo. No undelete. Only your last backup, if you had one. Gemini CLI destroyed a user's project files the same way. Replit's agent dropped a production database. These are not hypotheticals -- they are documented and all within the last year. Data and credentials die. The hardware survives them.
You can defend the host by hand: never run --dangerously-skip-permissions on your machine, never give the agent passwordless sudo, never pre-approve broad globs like Bash(sudo *) or Bash(*) and keep real backups. That is a lot of discipline to maintain on every project, forever, at 2am.
So I built Cleat. Same unrestricted power, but inside a per-project Docker sandbox where, by default, the blast radius stops at the container. Your host system stays untouched. Capabilities -- ssh, git, env, gh, docker -- are all off until you opt in. Claude can rm -rf / inside the container and the rest of your Mac won't even notice. Give the agent a cage, not your keys.
We haven't leaked a key, lost a home folder, or restored from a backup since.
curl -fsSL https://cleat.sh/install | bash
This clones the repo to ~/.cleat, checks out the latest stable release tag and symlinks cleat into your PATH. The short URL resolves to the same install.sh served from the latest tagged release on GitHub.
git clone https://github.com/cleatdev/cleat.git
cd cleat
./install.sh --local
This symlinks your working copy into PATH. Edits to bin/cleat take effect immediately, no reinstall needed. Switch back to the official release at any time with ./install.sh (without --local).
Releases are published as git tags (e.g. v0.1.0). The updater fetches tags and checks out the latest one:
cleat update
To update just the Claude Code build bundled in the image (without a full rebuild):
cleat upgrade-claude # latest (default)
cleat upgrade-claude stable # stable channel
cleat upgrade-claude 2.1.156 # pin a version
This re-runs the official installer in the image and commits it back, then offers to recreate the current project's container so the new version takes effect immediately. The change is local-only. cleat rebuild/update/nuke reset the image to a fresh release build (which already bundles a current Claude Code).
You don't have to remember to run it: when you start cleat interactively, it checks (at most once every 10 minutes) whether a newer Claude Code is out and offers to upgrade before starting. The check is skipped for non-interactive runs, never blocks on a slow network, defaults to the latest channel (CLEAT_CLAUDE_CHANNEL=stable to change it) and can be turned off with CLEAT_NO_CLAUDE_UPDATE_CHECK=1.
To rebuild the whole image from scratch instead:
cleat rebuild
cd ~/your-project
cleat # starts the container + launches Claude
# Claude will prompt you to log in on first run
Or authenticate separately:
cleat start # start the container
cleat login # opens a browser URL to sign in
Credentials are saved to ~/.claude on your host and shared across all containers automatically. Log in once, every container picks it up: whether you signed in on the host or inside any box, the next box you start or create carries the login. On macOS, where Claude keeps its login in the Keychain rather than a file, Cleat bridges that token into the box for you on launch.
cd ~/your-project
cleat
That's it. You're inside Claude Code with full autonomous permissions, sandboxed in Docker.
# Start a new session
cd ~/my-project
cleat
# Resume your last session
cleat resume
# Check what's running
cleat ps
# Stop when done (keeps container for resume)
cleat stop
# Remove the container when you want a fresh environment.
# Session history lives on the host at ~/.claude/projects/<key>/
# and is NOT touched by cleat rm. `cleat resume` after rm
# auto-creates a fresh container and picks up where you left off.
cleat rm
cleat resume
Each project gets its own isolated container:
# Terminal 1
cd ~/backend && cleat
# Terminal 2
cd ~/frontend && cleat
# See all running containers
cleat ps
Cleat containers:
● cleat-backend-1a2b3c4d
Up 12 minutes
/Users/you/backend
● cleat-frontend-5e6f7a8b
Up 3 minutes
/Users/you/frontend
A box is a named, isolated container scoped to the current directory. Every
box mounts the same live files, but each has its own capabilities, writable
layer and Claude session, so a locked-down dev box can run beside a
cloud-capable az box over the same repo. The agent in dev can't reach the
Docker socket or cloud token that az holds.
cleat start # the default box (main)
cleat start az --desc "cloud box" # a separate az sandbox
cleat config az --enable docker # give just the az box the docker cap
cleat resume dev # resume the dev box's last session
cleat status # list this project's boxes
The token after a verb is a box name (lowercase letters, digits, -, _),
never a path. cleat always operates on the current directory. The default box
is byte-identical to the pre-boxes container, so existing projects keep working
unchanged. Per-box caps come from .cleat.<box> (replace, not merge: a box can
have fewer caps than .cleat). One caveat: ~/.claude (your Anthropic auth)
is shared across boxes. A box isolates host capabilities and the writable layer,
not your Claude login.
A kit is a curated Claude Code setup (a CLAUDE.md policy plus custom
subagents) that you enable for one box with one command. The flagship kit,
plan-big-execute-small, adapts the coordinator pattern from
Anthropic's cookbook
(big models for planning, small models for execution):
run your session on Fable 5 (set once with /model inside the session) and
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. Prefer different economics? Pin or swap the agent
models under a [kits] section in ~/.config/cleat/config
(worker_model = haiku). The planner is always your session's model.
cleat kit # interactive picker: kit, then models
cleat kit list # plain library + this project's selections
cleat kit plan-big-execute-small # or enable directly, for the main box
cleat kit off # back to your own config next session
cleat kit show plan-big-execute-small # read every line it injects, first
A kit merges on top of your own config inside the box, so your global
CLAUDE.md and agents keep working: the kit section is appended and clearly
marked, after a Cleat box notes section every box carries with the
clipboard-bridge rules. Its content stays off the host: kits live in generated
mask files mounted into the box, nothing kit-related lands in your ~/.claude
and native claude never sees them. (Creating a box does seed inert placeholders there
when missing, an empty CLAUDE.md and empty agents/commands dirs: mount
targets, not content.) Different boxes can run different kits on the same
repo. Kits contain instructions and subagents only, no hooks and no settings.
Whatever they steer the agent to do happens inside the cage. As a
hardening side effect, your three user-level instruction surfaces
(~/.claude/CLAUDE.md, agents and commands) are mounted read-only in
every box: the agent reads them but can't plant a host-user-level command or
agent that your host claude would later obey. Author those at project level
(.claude/agents/, .claude/commands/) instead. The read-only copies
dereference symlinks (a dotfile-repo commands dir shows up as real files in
the box), a broken symlink at one of the three paths stops box create with a
clear fix-or-remove error (your symlink is never deleted) and a box created
before these masks existed prints a recreate note on every start until you
run cleat rm && cleat.
| Command | Description |
|---|---|
cleat | Build + run + launch Claude Code (all-in-one) |
cleat resume | Resume the most recent session (recreates the container if cleat rm was run since: sessions persist on the host) |
| Command | Description |
|---|---|
cleat stop [box] | Stop this project's container (keeps it for resume) |
cleat rm [box] | Stop and remove container permanently (session history on the host is preserved) |
cleat stop-all | Stop all Cleat containers |
cleat build | Build the Docker image |
cleat rebuild | Force rebuild the image from scratch |
cleat upgrade-claude [stable|latest|VERSION] | Update the bundled Claude Code in place (default latest). Offers to recreate the current container |
cleat clean | Stop everything and remove the image |
cleat prune | Remove stale cleat images (boxes and other projects untouched) |
cleat prune --cache | Also clear the shared Docker build cache (regenerable, all projects). Typed flag + default-No confirm |
cleat storage | Read-only Docker disk breakdown: fill bar, cleat vs shared vs other projects |
cleat nuke | Remove all Cleat containers and images, plus the shared build cache |
| Command | Description |
|---|---|
cleat config | Open the .cleat editor: a keyboard TUI for capabilities + resources |
cleat config --list | List capabilities and resources (memory, cpus) and their status |
cleat config --enable <cap> | Enable a capability (e.g. git, ssh, env) |
cleat config --disable <cap> | Disable a capability |
cleat config --memory <val> | Set the box memory ceiling (default clears it) |
cleat config --cpus <val> | Set the box CPU limit (all clears it) |
cleat config --project --enable <cap> | Project-level config (saved to .cleat) |
cleat config <box> --enable <cap> | Per-box config (saved to .cleat.<box>, replaces .cleat for that box) |
The editor also has a generate row (global scope): it stamps your current caps + resources into ./.cleat so a per-project config never has to be hand-written. It preserves an existing [setup] section and does not auto-trust (the file goes through the normal trust prompt on the next run).
| Command | Description |
|---|---|
cleat trust [path] [box] | Record approval for a project's (or a box's) .cleat capabilities and [setup] |
cleat trust [box] | Trust a box of the current project (a lone valid box name) |
cleat trust --list | List trusted projects and boxes (yellow = config changed since approval) |
cleat untrust [path] [box] | Remove a project's (or a box's) trust entry |
| Command | Description |
|---|---|
cleat kit | Interactive picker: pick a kit, then its agent models (TUI, like cleat config) |
cleat kit list | Plain kit library and this project's selections |
cleat kit <name> [box] | Enable a kit for a box (merges on top of your config, next session) |
cleat kit off [box] | Disable the box's kit (back to your own config) |
cleat kit show <name> | Print everything a kit injects |
| Command | Description |
|---|---|
cleat setup [box] | Run this project's [setup] provisioning now (the box must already be running) |
cleat setup [box] --show | Preview the payload, trust state and marker state without running anything |
start, run, resume, claude, shell, login)| Flag | Description |
|---|---|
--cap <name> | Enable a capability for this session only |
--env KEY=VALUE | Pass environment variable to container |
--env KEY | Inherit from host environment |
--env-file PATH | Load env vars from file |
--trust-project | Auto-approve the current project's .cleat caps without prompting |
--trust-setup | Auto-approve the current project's [setup] provisioning without prompting |
--desc <text> | Set the box's description at start (host-side, never recreates) |
| Command | Description |
|---|---|
cleat claude [box] | Attach Claude Code to a running container |
cleat shell [box] | Open bash inside the container |
cleat login [box] | Authenticate with Anthropic (OAuth) |
cleat logs [box] | Tail container logs |
| Command | Description |
|---|---|
cleat status | Show this project's boxes, image and auth status |
cleat describe [box] [text] | Show or set a box's description (host-side, never recreates) |
cleat ps | List all Cleat containers (running and stopped, with a box column) |
cleat update | Check for updates and install the latest version |
cleat version | Show current version |
All commands operate on the current working directory. The optional [box] is a
named sandbox for the project (default: main). See Boxes above.
┌──────────────────────────────────────────────────────────────┐
│ Your machine │
│ │
│ ~/.claude ──────────────┐ (auth, sessions, settings) │
│ ~/.claude.json ─────────┼── (config) │
│ ~/my-project ───────────┼──────────────────────┐ │
│ │ │ │
│ ┌────────────────────────┼──────────────────────┼───────┐ │
│ │ Docker container │ │ │ │
│ │ v v │ │
│ │ /home/coder/.claude /workspace │ │
│ │ /home/coder/.claude.json │ │
│ │ │ │
│ │ Claude Code (--dangerously-skip-permissions) │ │
│ │ │ │
│ │ Can: read/write project, install packages, run cmds │ │
│ │ Cannot: touch host system, access other projects │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
| File | Purpose |
|---|---|
bin/cleat | CLI script (symlinked as cleat) |
docker/Dockerfile | Node.js bookworm-slim image with Claude Code (native installer) |
docker/entrypoint.sh | Maps host UID/GID into the container so files are owned by you |
docker/clip | Clipboard shim -- writes to file bridge (primary) or OSC 52 daemon (fallback). Symlinked as pbcopy, xclip, xsel |
docker/clip-daemon | Background daemon -- relays clipboard data to the host terminal via OSC 52 (fallback for terminals that support it) |
docker/CLAUDE.md | User-level instructions for Claude Code (clipboard usage, paste limitations) |
install.sh | One-line installer (curl | bash) |
cleatcleat-<dirname>-<hash> (hash derived from the full project path) with your project mounted at /workspace~/.claude for shared authentication across all containerspbcopy/xclip/xsel relay to your host clipboard--dangerously-skip-permissions inside the sandboxContainers run with these protections by default:
--pids-limit 4096 -- prevents fork bombs from affecting the hostcleat config (the arrow-key editor has a Resources group) or cleat config --memory 4g --cpus 2, or hand-write a [resources] section in ~/.config/cleat/config or <project>/.cleat. The editor's choices are built from your actual machine. Cpus come from the core count Docker reports. On a VM bigger than 8 GB the memory ring climbs in real stops to the VM's size, so a 24 GB VM can be asked for all 24 GB. Past 8 GB it annotates instead of blocking, with a note that gets blunter as the number climbs and the full reason at the whole-VM value (a box that grows into everything starves the daemon and the VM's own OOM killer starts firing). Repo-supplied values are capped (8g memory, your core count for cpus). CPU is unlimited unless you set it -- an idle core costs nothing. If a session is ever OOM-killed (often a test runner spawning one worker per host core), Cleat says so and how to fix it: raise memory, cap workers (jest --maxWorkers=2), or set cpus--init -- a real PID 1 reaps orphaned processes, so long sessions can't wedge on zombie buildup and cleat stop is instantImages are published multi-arch (amd64 + arm64): Apple Silicon runs natively, never under emulation. cleat prune clears cleat's own stale images (cleat also offers this automatically when they pile up). Boxes and other projects' images are never touched.
Closing a terminal ends the session but leaves the box running, still reserving its memory ceiling. On every interactive start, Cleat stops other idle boxes that are safe to stop (detached, no agent running, idle past a 30-minute grace) and tells you what it freed. A box working unattended (terminal left open, agent still running) is never touched. Disable with CLEAT_NO_IDLE_SWEEP=1. Tune the grace with CLEAT_IDLE_GRACE_MINS.
If your Docker VM memory is set too low, or swap is left at the default, Cleat holds the launch on a prominent amber banner and waits for you to press Enter, instead of letting the warning scroll past unread into Claude's TUI. It fires only on a genuine config problem (never the transient overload notice) and only on a real interactive terminal, so cron, pipes and CI never block. Press Enter to launch anyway, Ctrl-C to go fix Docker, or set CLEAT_NO_DOCKER_GATE=1 to keep the advisory but skip the hold.
Disk is watched the same way. Every box shares one Docker store, so a box that reads 100% full is really the whole store filling up. When it crosses about 85% full with little free space Cleat drops a one-line advisory naming what you can reclaim (cleat storage shows the full breakdown, cleat prune --cache clears the shared build cache). When it crosses 95% with under 10 GB free it holds the launch like the memory gate, with CLEAT_NO_DISK_GATE=1 to skip the hold. The trigger is the fill percentage, so a 60 GB disk and a 1.8 TB one trip at the same fullness. The fix guidance is written for your engine (Docker Desktop, OrbStack, Colima, native Linux or WSL). If a store is too full for a box to start at all, Cleat catches the out-of-space error and prints the same guidance.
Capabilities are opt-in features that extend what the container can access from the host. They are disabled by default: the baseline container is locked down and each capability explicitly widens the boundary.
# Interactive wizard
cleat config
# Direct mode
cleat config --enable git
cleat config --enable ssh
cleat config --enable env
# One-off (session only, no config change)
cleat --cap ssh start
| Capability | Category | What it does |
|---|---|---|
git | mount | Mounts ~/.gitconfig (read-only). Commits inside the container use your host identity. |
ssh | mount | Mounts ~/.ssh (read-only). SSH agent forwarding if SSH_AUTH_SOCK is set. |
env | mount | Auto-loads env vars from ~/.config/cleat/env (global) and .cleat.env (project). |
hooks | mount | Runs your Claude Code hooks on the host (global and project-level). |
gh | mount | Mounts ~/.config/gh (read-write). gh auth login inside container writes tokens to host. |
docker | sandbox | Mounts /var/run/docker.sock. docker, docker compose and anything that talks to the daemon run against your host: sibling containers, zero overhead. Sandbox-escaping. See security note below. |
Cloud CLI caps (
az,aws,gcloud) and the lazy-install framework that backed them shipped in v0.11.0 / v0.12.0 and were removed after v0.12.3. They bloated first-run time without earning their weight. Install the CLI on the host and pass credentials via theenvcap.
The post-launch summary and cleat status group active caps by behavior:
git, ssh, env, hooks, gh. Bind-mount auth/identity, no install.docker. Mounts the host socket, breaks isolation.When only one category is active the line collapses to a single coloured row. With caps in both categories, the renderer prints a labeled block: same UI on the landing page mockups so the CLI and the marketing copy stay in lockstep.
.cleat approvalA project's .cleat file lives in the repo. Whoever controls the repo controls that file. Cleat won't silently apply a .cleat's capabilities on first run. Instead, on first launch inside a project with a .cleat, you'll see:
┌─────────────────────────────────────────────────────────────────────┐
│ This project's .cleat file requests capabilities │
│ that extend what the sandbox can access on your host. │
│ │
│ Requested: │
│ │
│ docker Host Docker socket (breaks sandbox) to test Docker apps │
│ env Load env vars from ~/.config/cleat/env and .cleat.env │
│ │
│ Project: /Users/you/proj │
└─────────────────────────────────────────────────────────────────────┘
Trust this project's .cleat? [y/N]:
Say yes and the approval is stored at ~/.config/cleat/trust. Next launch, nothing to see. Cleat silently applies the caps.
Approval is keyed on the canonical list of capabilities declared in .cleat, not the raw file. Comment edits and cap reordering don't invalidate trust. Adding, removing, or changing a cap triggers a re-prompt with an "…has changed since you trusted it" framing.
Non-interactive contexts (pipes, CI, cleat … | tee log) can't answer a prompt, so they default-deny: project .cleat caps are silently dropped, global config and --cap flags still apply. To opt in explicitly:
CLEAT_TRUST_SETUP=1 (or --trust-setup) approves a project's [setup] commands only. CLEAT_TRUST_PROJECT=1 never covers it: caps and setup are separate consent classes.
cleat --trust-project # one-off session flag, caps only
CLEAT_TRUST_PROJECT=1 cleat # env var (same effect)
cleat --trust-setup # one-off session flag, [setup] commands only
CLEAT_TRUST_SETUP=1 cleat # env var (same effect)
cleat trust # persist for this project, once (caps and setup)
cleat trust # trust the current dir's .cleat
cleat trust ~/proj # trust a specific project
cleat trust web # trust the current project's "web" box (.cleat.web)
cleat trust --list # show all trusted projects and boxes
cleat untrust web # untrust just the "web" box
cleat untrust ~/proj # remove a project's trust entry
| Source | Trusted? |
|---|---|
~/.config/cleat/config (global) | ✔ always: user's own file |
--cap <name> CLI flag | ✔ always: affirmative typed action |
<project>/.cleat | requires approval per-project, per-cap-set |
[setup] in <project>/.cleat | requires approval per-project, a separate consent class from caps |
cleat status never prompts: it's read-only and silently omits untrusted project caps when displaying.
[setup] sectionSome stacks need a tool the base image doesn't ship: a runtime, an SDK, a
database client. A [setup] section in .cleat lists the shell commands that
install it, run once per container as the coder user right after it's
created. You approve the exact commands once, the same way you approve
capabilities.
[setup]
curl -fsSL https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -o /tmp/msprod.deb
sudo dpkg -i /tmp/msprod.deb
sudo apt-get update
sudo apt-get install -y dotnet-sdk-8.0
That snippet shows the syntax on amd64. Microsoft's Debian feed ships .NET 8
for amd64 only, so the runnable examples/setup/dotnet
example uses the official dotnet-install.sh on arm64 (Apple Silicon).
A script <path> line inlines a project-relative script file at that position
instead of writing commands inline. List as many script directives as you
like, mixed with inline commands, in any order. Copy-paste examples live in
examples/setup/: dotnet (inline commands), python (one
script file) and rust (two script files).
Setup trust is separate from capability trust. CLEAT_TRUST_SETUP=1 (or
--trust-setup) approves it non-interactively, cleat trust approves both
caps and setup together and editing [setup] re-prompts without ever
recreating the container. Trust is per (project, box): cleat trust <box>
approves that box's .cleat.<box>.
A failed command prints its exit code and the box still opens. Fix .cleat
or the box, then retry with cleat setup.
When docker is enabled, the container mounts the host Docker socket and can build, run and manage containers against the host daemon. Containers you launch from inside Cleat run as siblings on the host (not nested), so there's zero virtualization overhead:
cleat config --enable docker # persistent
cleat --cap docker # one-off session
# Then, inside the sandbox:
docker compose up -d
docker compose exec app npm run test:ci
docker build -t myapp .
docker run -v $(pwd):/app node:24 npm install
Cleat also bind-mounts your project at its host path inside the container (in addition to /workspace) and sets workdir there, so $(pwd) returns a host-valid path. This makes docker run -v $(pwd):/app … and relative paths like -v ./data:/data in docker-compose.yml resolve correctly on the host daemon.
The CLEAT_HOST_PROJECT environment variable is exported with your project's host path for scripts that want it explicitly.
Security note. The Docker socket grants root-equivalent access to your host. Any process inside the container that can reach
/var/run/docker.sockcan create a container that mounts/from the host and escape the sandbox (this is a property of Docker, not Cleat). When the capability is active, Cleat prints an amber warning on startup:hljs language-css! Docker socket mounted. Container can create host-level processesEnable this capability only in projects you trust and disable it when you don't need it. It's off by default and every activation is explicit (
cleat config --enable dockeror--cap docker).
Known limitations in v0.10.0:
/workspace/… paths in -v aren't translated. Docker errors cleanly that the source doesn't exist. Use $(pwd) or the host path instead./tmp/scratch after mkdir -p /tmp/scratch inside Cleat) will be created on the host as empty directories. Keep bind-mount sources under your project path.Daemon down after a reboot? Run cleat and it starts Docker for you, waits
with a spinner, then continues your command. On macOS that means Docker
Desktop, OrbStack, or Colima (named Colima profiles included), on Linux Docker
Desktop or a rootless engine via systemctl --user, from WSL2 the
Windows-side Docker Desktop when interop is enabled. Where it can't start Docker safely it prints the
exact fix instead: a root-owned Linux engine (or an in-distro engine inside
WSL2, which wins over the Windows Desktop) gets sudo systemctl start docker,
a socket you can't write means Docker is up and you're not in the docker
group (the message hands you sudo usermod -aG docker <user>) and a remote
endpoint (tcp://, ssh://, npipe://, fd://) is refused with "start it
where it runs". Fires only on session verbs and only in an interactive
terminal, so scripts and CI are untouched. The wait is bounded
(CLEAT_AUTOSTART_TIMEOUT_SECS, default 90s) and CLEAT_NO_AUTOSTART=1
turns it off.
No Docker installed at all? Cleat offers to install it, consent-first: on macOS a menu of Docker Desktop / OrbStack / Colima via Homebrew's official packages (casks for Desktop/OrbStack, formulae for Colima, with the licensing difference stated), on Linux Docker's official install script downloaded to a private temp dir and run under sudo only after you say yes, on WSL2 the Windows-side Desktop via winget. The exact command is always shown, the default is No and scripts are never prompted.
The env capability controls automatic loading of env files. The --env and --env-file flags always work, regardless of whether the capability is enabled:
# These always work (bypass capability gate)
cleat --env GH_TOKEN=abc123 start
cleat --env GH_TOKEN start # inherit from host
cleat --env-file .env.local start
# These require the env capability
# ~/.config/cleat/env ← global
# .cleat.env ← project-specific
When you change capabilities or env keys after a container was created, Cleat detects the mismatch the next time you run cleat, cleat resume, or cleat claude. On a TTY it prompts you to recreate (a plain-text line, no box):
▸ Config changed since cleat-<project> was created: caps or env keys differ from the running setup
Recreate cleat-<project> now? [Y/n]
Accepting removes the container and rebuilds it with the new caps/env. Sessions persist on the host (~/.claude/projects/<key>/) and are never touched. Declining keeps the existing container.
A Cleat version bump on its own does not trigger this: the drift check looks only at caps and env keys. Image freshness is handled separately and is also content-aware: the on-start image-refresh prompt fires only when the image's actual contents change (the entrypoint, the clipboard or browser bridge, the Dockerfile, or the pinned base), not on every version bump. The base image is pinned by digest, so a routine release leaves your container and everything you installed in it untouched. A base or security update ships through the same refresh prompt.
Non-TTY runs (CI, scripts) print the notice and continue with the existing container. They never auto-destroy.
~/.config/cleat/config ← global capabilities
~/.config/cleat/env ← global env vars
<project>/.cleat ← project-level capabilities (extends global)
<project>/.cleat.env ← project-level env vars
Cleat uses a clean, consistent output format with no Docker noise.
✔ Image ready (cached)
✔ Container started
✔ Auth shared
✔ Claude launched
Container: cleat-backend-a1b2c3d4
Project: ~/backend → /workspace
Caps: git, ssh
Slow operations (image build, container start) show animated braille spinners that resolve to checkmarks. When stdout is not a TTY (piped, CI), spinners degrade to static lines.
✔ Session ended. Resume with: cleat resume
Docker's "What's next?" promo text and clipboard watcher cleanup messages are suppressed.
When the hooks capability is enabled, your Claude Code hooks run on the host, exactly as if you weren't using a container. Hooks from all three settings locations are supported:
~/.claude/settings.json (global).claude/settings.json (project, committed).claude/settings.local.json (project, local)cleat config --enable hooks # enable persistently
cleat --cap hooks start # enable for one session
osascript, local scripts and anything host-specific work transparentlyWhen Claude Code or any tool inside the container calls open or xdg-open with a URL, it opens in your host browser. OAuth callbacks are automatically proxied back to the container. cleat login and any auth flow work seamlessly without manual URL copy-paste. No capability needed.
One click, one tab. Your terminal already opens a clicked link itself, so on an interactive terminal the bridge defers plain links to it and opens via the bridge only what the terminal won't: auth/OAuth-callback URLs and non-interactive runs. Clicking a link opens a single tab, on any terminal. Override with CLEAT_BROWSER_BRIDGE=always (open every URL through the bridge) or off (never auto-open, the login callback proxy still runs).
Containers can always reach services on the host via host.docker.internal. No capability needed. User-defined hooks, MCP servers and HTTP endpoints on the host work out of the box.
# In .cleat.env (with env capability enabled)
CLAUDE_VISUAL_URL=http://host.docker.internal:3200
On Linux (Docker Engine), Cleat adds --add-host host.docker.internal:host-gateway automatically. Docker Desktop (macOS/Windows) provides this natively.
Cleat checks for new release tags at most once every 10 minutes via git ls-remote --tags (a lightweight network call that fetches no objects). When a newer version is available, you'll see a notice before Claude Code launches:
┌──────────────────────────────────────────────────────┐
│ Update available v0.4.0 → v0.5.0 │
│ Run cleat update to install the latest version. │
└──────────────────────────────────────────────────────┘
.update_check inside the installation directory (~/.cleat).cleat update. To also update Claude Code inside containers, follow up with cleat rebuild.Clipboard works out of the box. When Claude Code (or any tool) calls pbcopy, xclip, or xsel inside the container, the text is copied to your host machine's clipboard -- no X11, display server, or special terminal features required.
A host-side clipboard watcher starts automatically alongside every Claude Code session. The container writes clipboard data to a shared file via a bind mount. The watcher claims the file and copies the content to your real clipboard using pbcopy (macOS), xclip, xsel, or wl-copy (Linux). Each copy is picked up once and never replayed: the watcher consumes the payload as it delivers it and discards anything left over from an earlier session instead of replaying it onto your clipboard.
┌─────────────────────────────┐ ┌─────────────────────────────────┐
│ Docker container │ │ Host │
│ │ │ │
│ Claude Code │ │ │
│ └─ echo "text" | pbcopy │ │ │
│ └─ writes to ──────────────> ~/.config/cleat/run/<box>/clip/│
│ /tmp/cleat-clip/ │ │ └─ watcher claims the file │
│ │ │ └─ pbcopy / xclip │
│ │ │ └─ ✔ clipboard! │
└─────────────────────────────┘ └─────────────────────────────────┘
An OSC 52 fallback is available for terminals that support it, used automatically when the file bridge is not active.
# These all work inside the container -- including from Claude Code:
echo "hello" | clip # dedicated helper
echo "hello" | pbcopy # macOS-style
echo "hello" | xclip -selection clipboard # Linux-style
echo "hello" | xsel --clipboard # Linux-style (alternative)
git log -1 --format=%B | clip # copy last commit message
Limits: Payloads are capped at 100KB. Paste (xclip -o, xsel --output, pbpaste) is not supported -- clipboard is copy-only.
If pbcopy/xclip/xsel inside the container doesn't copy to your host clipboard:
ls /tmp/cleat-clip/.host-ready. If the file exists, the host watcher is running.pbcopy (macOS), xclip, xsel, or wl-copy (Linux) available on your PATH.cleat rm && cleat start so the new clipboard mount is created./workspace and copy from the host.An interactive cleat starts Docker for you (see Docker autopilot above), so
you rarely see a raw daemon error now. If Cleat prints Docker isn't running, run
the exact start command it shows. This happens by design when the auto-launch can't
help: a script or CI run (no TTY), CLEAT_NO_AUTOSTART=1, a root-owned Linux engine
or an in-distro WSL2 engine (the message hands you sudo systemctl start docker), a
WSL2 distro with interop disabled (start Docker Desktop on Windows and enable WSL
integration for the distro), or a remote tcp:///ssh:///npipe:///fd://
endpoint (start it where it runs). Docker is running, but you can't reach its socket is permission, not a down daemon: add yourself to the docker group with the
printed sudo usermod -aG docker <user>, then log out and back in. Docker isn't installed instead? Take the install offer, or run the printed install command.
# If /usr/local/bin is not writable, the installer uses sudo automatically.
# You can also install to a custom location:
ln -sf "$(pwd)/bin/cleat" ~/.local/bin/cleat
Each container is named cleat-<dirname>-<hash> where the hash is derived from the full absolute path of the project directory. This means two projects with the same directory name (e.g. ~/code/client-a/api and ~/code/client-b/api) get separate containers automatically. The container name is printed before every session so you always know which sandbox you're in.
The Claude Code CLI is baked into the Docker image. To get the latest version:
cleat rebuild
This shouldn't happen. The entrypoint maps your host UID/GID. If it does, check that Docker is passing through HOST_UID and HOST_GID correctly:
cleat shell
id # should show your UID/GID
cleat clean # remove all containers + image
cleat uninstall # remove CLI symlinks
rm -rf ~/.cleat # remove the repo clone
Your project files and ~/.claude credentials are never touched.
Contributions are welcome! Please open an issue or submit a pull request.
git clone https://github.com/cleatdev/cleat.git
cd cleat
# Make your changes on main, test locally
./bin/cleat start ~/some-test-project
Releases are cut by tagging a commit on main:
git tag v0.3.0
git push --tags
The installer and updater both resolve the latest semver tag automatically. No release branch is needed.
Cleat. Give the agent a cage, not your keys. | Docker sandbox for AI coding agents | cleat.sh
⚠️ Experimentelle Skill-Sammlung für deutsches Recht (Arbeits-, Gesellschafts-, Insolvenz-, Datenschutz-, Prozessrecht u
Manage multiple Claude Code agents from TUI or Web with tmux and git worktrees
Project management using GitHub Issues + Git worktrees for parallel agent execution
Core skills library for Claude Code with 20+ battle-tested skills including TDD, debugging, and brainstorming