sbclaude
sbclaude runs Claude Code inside a throwaway Docker container with the bash sandbox
and permission prompts disabled, against a configurable set of host bind mounts. The
container stores nothing of its own (--rm); everything lives on the host. You only ever
invoke sbclaude — it manages its own image and containers via the Docker SDK.
There is a single image, sbclaude, built on demand. It bundles everyday coding tools
(Debian slim + git, ripgrep, Node 24/Yarn, a C toolchain, gh, glab, uv, jq), formatters and
linters kept at their latest upstream release (clang-format, jsonnet, jsonnetfmt, shellcheck), Qt 6
development (qt6-base-dev plus ninja), and a mobile reverse-engineering toolchain (a JDK,
frida, mitmproxy, dex2jar, baksmali/smali, and launchers for the host-mounted Ghidra,
Android SDK, jadx, and apktool).
How it works
- The
claudeexecutable is not installed in the image. It is a self-contained native binary (Node is bundled in), so the host copy is bind-mounted read-only at run time — the image always tracks whatever version the host has. (Only glibc ≥ 2.17 is needed, which is why the base is Debian, not Alpine/musl.) - Your identity is mirrored:
UID/GID/USER/HOMEare passed in and an entrypoint recreates that user inside the container. This means:- mounted
~/.claudefiles (incl. the0600.credentials.json) are owned correctly; - paths resolve identically — a host path you paste into a prompt
(
/home/you/dev/foo) is bind-mounted at that same absolute path inside the box.
- mounted
--dangerously-skip-permissionsis passed (hence the non-root user — that flag refuses root), and a patched copy of yoursettings.jsonis mounted over the in-container one withsandbox.enabled=false,skipDangerousModePermissionPrompt=true(no bypass dialog) andtui="fullscreen". Your real settings file is never modified.- The image auto-builds on first use and rebuilds automatically when the packaged Dockerfile/entrypoint change (tracked via a content-hash label).
Install
uv tool install . # or: pipx install .
# the image builds itself on first `sbclaude run`; or pre-build:
sbclaude build
Usage
sbclaude # run claude; cwd is the project (writable)
sbclaude run -p ~/dev/foo # explicit project dir (writable, becomes workdir)
sbclaude run -r /data -w ~/scratch # extra read-only / read-write mounts
sbclaude run --re --x11 # Ghidra/Android mounts + GUI passthrough
sbclaude run -- --version # everything after -- goes to claude
sbclaude ls # list running sbclaude containers
sbclaude stop [--all] # stop this project's boxes (or all with --all)
sbclaude shell # root debug shell in this project's box
sbclaude build [--no-cache] # (re)build the image
sbclaude delete-image # remove the sbclaude image
sbclaude config # show the config file path
You can run several boxes against the same project directory at once. Each run gets a
unique container name — the project name plus a short random suffix — so there is no name
collision; override it with -n. sbclaude ls lists them all, sbclaude stop stops every box
for the current project, and sbclaude shell attaches to it (or asks you to pass -n NAME when
more than one is running).
run flags
| Flag | Effect |
|---|---|
--re |
enable --ghidra + --android together |
--ghidra |
mount host Ghidra (/usr/share/ghidra) read-only |
--android |
mount Android SDK + ~/.android + /dev/kvm (adb/emulator) |
--usb |
expose /dev/bus/usb for adb over USB |
--ios |
mount the host usbmuxd socket so frida reaches an iOS device over USB |
--x11 |
forward DISPLAY + XAUTHORITY for GUI apps (Ghidra GUI, jadx-gui, emulator) |
--ssh |
mount the host ~/.ssh read-only and forward the ssh-agent, for SSH git remotes |
--gpg |
mount the host GnuPG home + agent socket for signing commits |
--session-recover |
install cc-session-recover (auto-resume) into the project on start |
--net bridge |
isolate the box's network (default is host — localhost = your host) |
-r/-w/-p/-n/-i |
extra ro/rw mount, project, container name, image override |
Configuration
All options live under a [tool.sbclaude] table. They are read from the global file at
~/.config/sbclaude/config.toml (path from platformdirs) and, for run, overlaid with the
target project's pyproject.toml [tool.sbclaude] table — project values win, so a repo can
pin its own defaults. All keys optional:
[tool.sbclaude]
gpg = true # mount the GnuPG home + agent for signing
network = "host" # default; "bridge" to isolate the box's network
re = true # enable the Ghidra + Android mounts together
ssh = true # mount ~/.ssh read-only + forward the ssh-agent for SSH git remotes
x11 = true # forward X11 for GUI apps
# image = "custom:latest" # force a different image
# debian_mirror = "http://ftp.us.debian.org/debian" # apt mirror for image builds
# memory = "8g" # override the auto host-RAM cap ("0" disables)
# cpus = "4" # cap CPUs (uncapped by default)
# recover = true # install cc-session-recover into every project (off by default)
pass_env = ["AWS_REGION"] # forward host vars (AWS_PROFILE is default)
ro = ["~/dev*", "~/ghidra_scripts", "~/Downloads"] # read-only mounts (globs + ~ ok)
rw = [] # the project dir is always rw automatically
[tool.sbclaude.env] # inject fixed vars (e.g. Amazon Bedrock)
CLAUDE_CODE_USE_BEDROCK = "1"
The toggle keys re, ghidra, android, usb, ios, x11, ssh, and gpg mirror the
matching run flags and default to false; setting one is the same as always passing that
flag.
Debian mirror — when deb.debian.org is slow, point image builds at a faster archive
mirror with debian_mirror (or --debian-mirror on build/run). Only the image's main
archive URI is rewritten; debian-security keeps pointing at deb.debian.org.
ro/rw accept globs (~/dev* → every matching dir) and ~; non-matching or
missing paths are dropped. Every path is mounted at its real absolute path, so a host
path you paste into a prompt resolves inside the box. The project (cwd or -p) is always
read-write and overlays any read-only parent (e.g. ~/dev ro + ~/dev/proj rw → proj
is writable).
Environment variables — inject with the [tool.sbclaude.env] table, forward host values
by name with pass_env, or per-run with -e KEY=VALUE (precedence: managed defaults <
[tool.sbclaude.env] < pass_env < -e). AWS_PROFILE is forwarded by default. This is how
you point the box at a different backend such as Amazon Bedrock
(CLAUDE_CODE_USE_BEDROCK=1 + your AWS_* vars; mount ~/.aws via ro if you use
profiles).
uv project environment — by default the box sets UV_PROJECT_ENVIRONMENT to a
container-local path (/tmp/sbclaude-uv-<project>), so uv sync/uv run build the
virtualenv there instead of writing .venv into your bind-mounted project. A host-built
.venv hard-codes the host interpreter path and would not resolve in the box anyway; the
container is throwaway, so the redirected env is rebuilt per run. Override it by setting
your own UV_PROJECT_ENVIRONMENT (via [env] or -e), or disable the behaviour entirely
with manage_uv_env = false. There is no clean equivalent for Node — node_modules is
fixed to the package root by Node's resolver (only Yarn Berry's YARN_NODE_LINKER=pnp
removes it, at the cost of changing module resolution), and node_modules built on the
box's Linux is usually reusable on a Linux host anyway, so it is left untouched.
Alternate config dir — if CLAUDE_CONFIG_DIR is set on the host, sbclaude mounts that
directory (its .claude.json, settings.json, history) and points claude at it inside the
box instead of ~/.claude.
Session recovery
cc-session-recover lets Claude Code pick a
long-running task back up after a quota or rate-limit pause. It is off by default — enable
it per run with --session-recover, or for every run with recover = true in the config. When
enabled, the container entrypoint runs the tool's install-into-project.sh against the project
before launching claude, which sets up its SessionStart and Stop hooks and a HANDOFF.md.
The tool is vendored into the image as a git clone (not the npm package) at
/opt/cc-session-recover, with PR #2
(safer watcher argument handling and the opt-in CC_REMIND_MODE prompt-injection limit) applied
on top. Two further patches keep the installer tidy: it no longer copies settings.example.json
into the project, and its own .gitignore handling is disabled. The installer writes into the
project's .claude/ (hooks and settings.local.json, merged with jq) and a HANDOFF.md;
because the project is bind-mounted read-write, those files land in your real repository and
persist, which is why this is opt-in. Afterwards the entrypoint appends the recovery artifacts
(HANDOFF.md, auto-continue.md, session-recover.js, session-recover.yaml,
standing-instructions.md, statusline-quota-cache.sh, and the three hook scripts) to the
project's .gitignore, each only when absent. If the install
fails, the box aborts rather than starting a session that silently lacks recovery.
MCP servers
MCP server configs live in your mounted ~/.claude.json, so they carry into the box — but
the server command must be runnable inside the container. A host Python venv won't
work: its bin/python symlinks to a host-only interpreter (e.g. /usr/bin/python3.13),
which doesn't exist in the box → ENOENT. The image ships Python 3 in a virtualenv at
/opt/venv (with the mcp SDK and pre-commit pre-installed) and uv, so point the
command at one of:
uv run /abs/path/to/server.py— best: reads the script's PEP 723 deps, works on the host too. Example:claude mcp add ghidra -- uv run ~/dev/ghidra-mcp/bridge_mcp_ghidra.py./opt/venv/bin/python3 /abs/path/to/server.py— the container's venv Python (hasmcppre-installed); container-only./opt/venv/binis first onPATH, so a barepython3resolves here too.
A server that talks to a process on the host (e.g. a Ghidra GUI on localhost) works out
of the box because the box defaults to host networking; pass --net bridge only if you
want to isolate it.
Git over SSH and commit signing
Pushing over SSH and signing commits need the host's private keys, which are not mounted
by default (the box is a fully-autonomous, no-prompt agent — see Hardening).
Opt in per run, or globally with ssh = true and gpg = true under [tool.sbclaude]:
--sshbind-mounts~/.sshread-only, so SSH remotes authenticate with the host's keys andknown_hosts. Read-only means newly-learnt host keys are not written back. It also forwards the host's ssh-agent:$SSH_AUTH_SOCKis bind-mounted at its own path and set in the box, so keys that are passphrase-protected or held in a hardware token still work — the host agent does the signing and the secret never enters the container. Without this a box holding only encrypted key files would stall on a passphrase prompt nothing can answer. Symlinks inside~/.sshare followed too: aconfig(or key) linked into a dotfiles repository has its target mounted read-only at the same path, so the link does not dangle and everyHostalias keeps working.--gpgbind-mounts the host GnuPG home (read-write —gpgneeds to write lock files and the trustdb) and overlays the host's live gpg-agent socket at~/.gnupg/S.gpg-agent. The host agent performs the signing and owns the secret keys, so a cached passphrase carries over and any pinentry prompt appears on the host. The image shipsgnupgandopenssh-client; your mounted~/.gitconfig(withuser.signingkey/commit.gpgsign) does the rest.
sbclaude run --ssh --gpg -p ~/dev/foo # inside: git push, git commit -S both work
Your ~/.gitconfig is always mounted read-only, along with every file it pulls in via an
[include] path = ... directive (resolved with git config --includes), so a split config
carries into the box intact.
RE toolchain (--re)
Mounted from the host (your exact versions): Ghidra (analyzeHeadless, ghidraRun),
Android SDK (adb, emulator, sdkmanager, avdmanager), jadx, apktool,
plus ~/.android (adb keys + AVDs) and /dev/kvm for emulator acceleration.
Installed in the image: Temurin JDK 21 (Ghidra 12 needs it), build-essential +
binutils, frida + frida-tools (pinned to the host version), mitmproxy,
dex2jar, baksmali/smali, CLI audio tools (ffmpeg, sox, flac, vorbis-tools,
opus-tools, lame, mpg123, wavpack, shntool, plus vgmstream-cli for game audio), CLI
image tools (ImageMagick, zbar, deark, pngdefry for -iphone PNGs),
czkawka-cli (duplicate/similar finder), and the X11/GL/audio libs the mounted GUI
binaries need.
sbclaude run --re --x11 -p ~/dev/some-apk-re
# inside: jadx -d work/jadx-out base/classes*.dex
# apktool d base -o work/axml-decoded
# analyzeHeadless ~/dev/x-re proj -import lib/arm64-v8a/foo.so
# adb devices ; emulator -avd ford-x86_64-api35 -writable-system &
# frida -U -f com.x.y -l hook.js
--x11 mounts /tmp/.X11-unix + your session xauth cookie and sets DISPLAY/
XAUTHORITY. Java/Swing (Ghidra) renders through XWayland (:0). If a GUI fails with a
cookie error, run on the host: xhost +SI:localuser:$USER.
--ios targets an iOS device attached to the host. The host runs usbmuxd (it owns
the USB device), and frida's usbmux backend reaches the device through that daemon's
socket — so instead of claiming raw USB (which would clash with the host usbmuxd),
--ios bind-mounts /var/run/usbmuxd plus the host's /var/lib/lockdown pairing
records. Combine with --re so frida is present:
sbclaude run --re --ios -p ~/dev/some-ios-re
# inside: frida-ls-devices # the host's device shows up over usbmux
# frida -U -f com.x.y -l hook.js
The host needs usbmuxd running and the device already paired (trusted). If frida sees
no device, confirm idevice_id -l works on the host first.
Hardening
This deliberately removes Claude's own sandbox and permission prompts, so the box leans on Docker for confinement instead. Every run is hardened by default (defense-in-depth — it limits blast radius, it is not a guarantee):
- Build time: minimal Debian slim,
--no-install-recommends+ cleaned apt lists, no secrets baked in (theclaudebinary and all auth are bind-mounted), OCI provenance labels, and all setuid/setgid bits stripped from the image. - Run time:
--security-opt no-new-privileges,--cap-drop ALLplus only the six caps the root entrypoint needs to create the mapped user, drop to it via gosu, and lettini(PID 1) forward signals such asSIGWINCHto the non-root child (CHOWN,DAC_OVERRIDE,FOWNER,KILL,SETUID,SETGID), a--pids-limit, a non-root mapped user, and the default seccomp/AppArmor profiles (never disabled). The claude process itself ends up with an empty effective capability set. - Can't lock the host:
--pids-limitstops fork bombs, and--memorywith an equal--memory-swapbounds RAM with no extra swap, so a runaway box is OOM-killed instead of thrashing the host into a freeze. The default cap is derived from host RAM (reserving the larger of 2 GiB or an eighth for the host); setmemoryto override or"0"to disable, andcpusto cap CPU (uncapped by default — saturation slows but does not lock). These are enforced by cgroups, i.e. the systemd cgroup driver on a systemd host — a separatesystemd-runwrapper is unnecessary (and would not bound the container, which runs under the Docker daemon's cgroup, not the CLI's).
Disable per run with --no-harden, globally with harden = false in config (this also
drops the resource caps), and add your own Docker flags (e.g. --read-only) via
docker_args = [...].
Still: the containerized Claude can run any command and read/write every mounted path
without asking, with unrestricted network. Keep writable mounts minimal (the config
defaults to read-only for everything but the project) and don't mount secrets you don't
want a fully-autonomous agent to touch. This is why --ssh and --gpg are opt-in: they
expose your private SSH and GPG key material (the GnuPG home read-write) to that agent.
Forwarding an agent socket does not hand over the secret itself, but it does let the box ask
the host agent to sign with any key it holds, for as long as the box runs.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sbclaude-0.0.1.tar.gz.
File metadata
- Download URL: sbclaude-0.0.1.tar.gz
- Upload date:
- Size: 42.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1de97b7e67741c1e6e0b91b92511349851d81e88742ccc72e9f3c51fc2a26ba9
|
|
| MD5 |
3311498622aee0a1c4789dc1fc564dcb
|
|
| BLAKE2b-256 |
d7d7c3fc664fcd15ab40ca03e1beb713b0e1aaeeb905349a0023951352bdb7cc
|
Provenance
The following attestation bundles were made for sbclaude-0.0.1.tar.gz:
Publisher:
publish.yml on Tatsh/sbclaude
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sbclaude-0.0.1.tar.gz -
Subject digest:
1de97b7e67741c1e6e0b91b92511349851d81e88742ccc72e9f3c51fc2a26ba9 - Sigstore transparency entry: 2364666690
- Sigstore integration time:
-
Permalink:
Tatsh/sbclaude@2411216156328307ca996f4007749dc5230fc5b4 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/Tatsh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2411216156328307ca996f4007749dc5230fc5b4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sbclaude-0.0.1-py3-none-any.whl.
File metadata
- Download URL: sbclaude-0.0.1-py3-none-any.whl
- Upload date:
- Size: 34.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d0ba9b493aace5f16a6a04d28416cc6a116b5bbfb6b89823b06507f39429d49
|
|
| MD5 |
10cc5155854867d060d6eddd4f2710d4
|
|
| BLAKE2b-256 |
d7bc1ee4d1544416e32ff62ee5336d3d4f88c346f3ac57acc1b1ece77eed0a2b
|
Provenance
The following attestation bundles were made for sbclaude-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on Tatsh/sbclaude
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sbclaude-0.0.1-py3-none-any.whl -
Subject digest:
2d0ba9b493aace5f16a6a04d28416cc6a116b5bbfb6b89823b06507f39429d49 - Sigstore transparency entry: 2364666776
- Sigstore integration time:
-
Permalink:
Tatsh/sbclaude@2411216156328307ca996f4007749dc5230fc5b4 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/Tatsh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2411216156328307ca996f4007749dc5230fc5b4 -
Trigger Event:
push
-
Statement type: