Runs Claude Code inside a Docker sandbox instead of directly on the host, while still behaving like a normal claude install: same ~/.claude config, same git identity (no credentials), same shell workflow. It works standalone, with no native Claude Code install required — see Requirements.
Claude Code can read and write anywhere it can reach, and run arbitrary shell commands. A container puts a hard wall around that: only the project directory and a short, explicit allowlist of mounts are visible inside, a filtered ~/.claude config, a git identity with its credential helpers stripped out, the host's CA bundle. Everything else, ~/.aws, ~/.ssh, other cloud CLI configs, simply isn't there. Tokens for services like gh or AWS only get in if you explicitly export and pass them through.
It doesn't take malice for that to matter, just a wrong command, or a manipulated one:
- A prompt-injection payload hidden in a dependency, README, or fetched file tells the agent to grab
gh auth tokenand slip it into a PR description. On the host, that hands over your GitHub session. In the container,ghisn't authenticated, so there's nothing to steal. - Debugging a failing deploy, Claude runs
aws sts get-caller-identityand pastes the output into a log or commit to explain what's wrong. On the host, that can leak live AWS keys. In the container,~/.awswas never mounted, so there's nothing to leak.
Each run is also disposable and reproducible: --rm plus a pinned toolchain (src/container/Dockerfile.claude-code) means stray global installs never accumulate on the host or drift between machines. Only the claude binary and login itself persist, via dedicated volumes: the binary self-updates, kept in sync with a native host install when there is one; the login is the container's own, independent of the host's (see How it works).
- macOS or Linux
bashjq(used to read hook scripts out of~/.claude/settings.jsonso they can be mounted into the container)- Docker
- Claude Code installed natively is optional. If
claudeis already onPATH,install.shlinks it asclaude-originaland the sandboxedclaudestays version-matched with it and seeds its first login from it. Without one, the sandboxedclaudestill works fully standalone: it logs in and self-updates on its own inside the container.
./install.shThis will:
- Check the OS and that Docker is available (warns, doesn't block, if Docker is missing).
- Locate the native
claudebinary viaPATH, if there is one. - Create
~/.secure-claude-code/bin/, containing aclaudesymlink tosrc/claude.sh, plus aclaude-originalsymlink to the native binary if step 2 found one. - Prepend
~/.secure-claude-code/bintoPATHin your shell startup files (whichever of.zshrc,.bashrc,.bash_profile,.profilealready exist), so it resolves before any native install. - Rebuild the
claude-code-sandboxDocker image fromsrc/container/Dockerfile.claude-code.
A native install, if one exists, is never touched, so its own auto-updater keeps working exactly as before. The symlink/PATH setup (steps 2-4) is idempotent and skipped when already installed, but the image rebuild in step 5 always runs. That makes re-running ./install.sh the supported way to pick up an edit to Dockerfile.claude-code: claude.sh on its own does not detect Dockerfile changes (see How it works). The script refuses to proceed if it finds a state it can't safely resolve on its own (e.g. a claude/claude-original in the shim directory that isn't a symlink it manages), explaining what to check.
Once installed (and after starting a new shell, so the updated PATH takes effect), use claude exactly as before:
claudeIt now runs sandboxed in Docker, with the project directory, your Claude config, and your git identity mounted in.
If a native install was found at install time, you can invoke the original, unconstrained claude binary directly:
claude-original./restore.shRemoves ~/.secure-claude-code/bin (the claude and claude-original symlinks) and the PATH entry added to your shell startup files. The native install was never modified, so claude resolves to it again as soon as you start a new shell.
src/claude.shis a wrapper that mounts the current project directory, your~/.claudeconfig, and git identity into the container, and runs the realclaudebinary inside it. It only builds theclaude-code-sandboximage itself when the image doesn't exist at all (e.g. right after adocker rmi); it never detects thatDockerfile.claude-codehas changed. Re-running./install.shis what rebuilds the image unconditionally (see Install), so that's the supported way to pick up a Dockerfile edit./home/node/.claudeitself is backed by a dedicated Docker volume (claude-code-sandbox-claude-dir-volume) that persists across--rmcontainers, with the host's individual~/.claudeentries (settings.json, agents, etc.) still bind-mounted on top of it as before. This is what lets the container keep its own Anthropic login, independent from the host's:.credentials.jsonis seeded once, on the volume's very first-ever run, from whatever credentials the host has, if any (macOS Keychain or~/.claude/.credentials.json); every run after that relies solely on the container's own copy, so a login or an OAuth token refresh performed inside the container actually persists. (A symlink from.claude/.credentials.jsoninto a separate credentials-only volume was tried first and doesn't work: Claude Code saves credentials via a temp-file-then-rename, and renaming onto a symlink path replaces the symlink itself with a plain file instead of writing through it, so the save was lost on the next--rm.) The practical effect is thatclaude(sandboxed) andclaude-original(native), when the latter exists, are logged in independently, and each re-authenticates on its own schedule; onlyclaude-originalwrites to the host Keychain/file.- Any hook script referenced by a
"command"hook in~/.claude/settings.json(e.g. a corporate compliance hook) is read-only bind-mounted into the container at the same path, so hooks configured on the host keep working unchanged inside the sandbox.claude.shreports which hook scripts got mounted, and warns about any it couldn't find on the host. - A native
claudeinstall, if one exists, is left completely untouched, so Claude Code's own auto-updater keeps managing it normally.docker-entrypoint.shkeeps the in-container copy in sync with whatever version the host is currently on, on every run, so the Docker image itself rarely needs rebuilding. Without a native install to track, the container instead self-updates to latest on every run. install.shcreates~/.secure-claude-code/bin/, with aclaudesymlink tosrc/claude.shand, if a native install was found, aclaude-originalsymlink to it, then prepends that directory toPATHin your shell startup files. Because it comes first onPATH, typingclaudeanywhere resolves to the sandboxed wrapper instead of any native binary, regardless of what its auto-updater does to it.restore.shundoes exactly that: removes the shim directory and thePATHentry.
src/
├── claude.sh # sandbox wrapper, installed as 'claude'
├── lib/
│ ├── path-utils.sh # symlink resolution helper
│ └── install-common.sh # shared helpers for install.sh / restore.sh
└── container/
├── Dockerfile.claude-code # sandbox image definition
└── docker-entrypoint.sh # keeps in-container claude version in sync with the host