Run the Claude Code CLI inside a rootless podman container. It sees one directory of yours and nothing else.
Everything is in a single script — the Containerfile and the container entrypoint are embedded in it, so one download is the whole install.
WARNING: Use at your own risk!
curl -fsSL https://raw.githubusercontent.com/torlenor/bclaude/main/bclaude | bash -s -- installThat drops bclaude into ~/.local/bin (pass a different directory as the last
argument). Then:
bclaude doctor # check podman is present and configured
cd ~/git/my-project && bclaude # first run builds the image, oncePrefer to read before you run? It's one file:
curl -fsSLO https://raw.githubusercontent.com/torlenor/bclaude/main/bclaude
less bclaude && chmod +x bclaude && ./bclaude installRequires rootless podman and nothing else on the host.
bclaude # interactive session on $PWD
bclaude -p "explain this repo" # claude flags pass straight through
bclaude -w ~/git/proj # mount a different directory
bclaude shell # a shell in the container instead
bclaude --ro -p "review this repo" # read-only workspace: it cannot write anything
bclaude --volume-per-project # config volume of its own for this repo
bclaude --allow-pkg # let Claude `sudo apt-get install` things
bclaude doctor # diagnose a broken setup
bclaude update # rebuild with the newest Claude Code
bclaude clean --all # remove image + every config volume + the loginbclaude help prints the full interface.
| Command | Effect |
|---|---|
(none) / run |
Run Claude Code on the workspace |
shell [args] |
Shell in the container (bclaude shell -c 'ls /work') |
build |
Build/rebuild the image |
update |
Rebuild from scratch with the newest Claude Code |
doctor |
Check podman, rootless setup, cgroups, subuid, image, credentials |
status |
Image, volumes, credential and workspace state |
install [DIR] |
Copy bclaude onto your PATH (default ~/.local/bin); also works piped from curl |
clean [--all] |
Remove the image; --all also offers to drop every config volume and the login |
clean --list |
List the auth volume and the config volumes, with the project each belongs to |
clean --prune |
Drop per-project volumes whose project directory is gone (--yes to skip the prompt) |
show containerfile / show entrypoint |
Print the embedded files (to inspect or fork) |
version |
Print the bclaude version |
A command name wins over a same-named claude subcommand — use bclaude run install to force something through to claude.
Every option has an env-var equivalent, so both bclaude -w ~/p and
BCLAUDE_WORKSPACE=~/p bclaude work. Note the BCLAUDE_ prefix on workspace,
volume, auth volume and image: bare WORKSPACE, VOLUME, AUTH_VOLUME and
IMAGE are deliberately ignored, since they pick what gets mounted and what
bclaude clean deletes — a stray export IMAGE= should not be able to aim
podman rmi -f at your own image.
| Option | Env | Default | Effect |
|---|---|---|---|
-w, --workspace DIR |
BCLAUDE_WORKSPACE |
$PWD |
Host dir mounted at /work — the only host path Claude can reach |
--ro |
BCLAUDE_RO=1 |
off | Mount the workspace read-only — Claude can read /work, never write it |
-V, --volume NAME |
BCLAUDE_VOLUME |
bclaude-config |
Named volume for ~/.claude (sessions, settings; not the login) |
--volume-per-project |
BCLAUDE_VOLUME_PER_PROJECT=1 |
off | A config volume per workspace (see Config volumes) |
--auth-volume NAME |
BCLAUDE_AUTH_VOLUME |
bclaude-auth |
Volume holding the login, shared by every project |
-i, --image REF |
BCLAUDE_IMAGE |
localhost/bclaude:latest |
Image to run |
--claude-version V |
CLAUDE_VERSION |
latest |
Claude Code npm version baked into the image |
--memory SIZE / --cpus N |
MEMORY / CPUS |
4g / 2 |
Resource caps; none or --no-limits disables |
TMPFS_SIZE |
512m |
Size of the nosuid tmpfs at /tmp |
|
--allow-pkg |
ALLOW_PKG=1 |
off | Passwordless sudo apt (relaxes two hardening flags — see below) |
--seed-config |
SEED_CONFIG=1 |
off | Copy host settings.json (model, tui, statusline) + statusline script in, rewriting host paths |
--seed-creds |
SEED_CREDS=1 |
off | Copy the host's Claude login into the auth volume (see Credentials) |
--no-git-config |
BCLAUDE_GIT_CONFIG=0 |
mounted | ~/.gitconfig is mounted read-only so git commit works inside |
--allow-root |
BCLAUDE_ALLOW_ROOT=1 |
off | Permit rootful podman — refused by default (see below) |
--rebuild / --no-cache |
off | Rebuild the image before running | |
--no-autobuild |
BCLAUDE_AUTOBUILD=0 |
off | Fail instead of auto-building a missing/stale image |
--dry-run |
off | Print the podman command instead of running it |
|
ANTHROPIC_API_KEY |
unset | Forwarded into the container if set — by name, so the value stays out of the podman command line |
- Rootless only — running as root is refused unless you pass
--allow-root. Rootless podman is what keeps a container escape from becoming host root. - Workspace —
/workis the only host project path mounted; everything else is throwaway container fs.--romounts it read-only for review sessions: Claude keeps working (config volume and/tmpstay writable), it just cannot change your files. --userns=keep-id:uid=1000,gid=1000maps your host user onto the image'sclaudeuser (uid/gid 1000), so files Claude writes in/workare owned by you — that's why the image renamesnode→claude. The explicituid=/gid=matters when your host uid isn't 1000: plainkeep-idwould leave the container process unable to write a workspace owned by you.- Config — lives in the
bclaude-configvolume, separate from your host~/.claude. The login is not in there; it sits in its ownbclaude-authvolume that every project shares. - Network — default rootless (pasta), unrestricted outbound: needed for npm/pip/git and the API.
- Hardening —
cap-drop=ALL,no-new-privileges, seccomp,pids-limit 2048, memory/cpu caps,nosuidtmpfs/tmp, setuid stripped from all butsudo. - Image freshness — the image is stamped with a hash of the embedded Containerfile + entrypoint, so editing the script rebuilds it on the next run.
- Portability — SELinux hosts get the
:zmount relabel automatically; hosts that can't enforce rootless cgroup limits get them dropped with a warning instead of a podman error; a TTY is only requested when there is one, sobclaude -p ...works in pipes and CI.
There are two volumes, and the split is the point:
bclaude-authholds nothing but the login, and every project shares it.bclaude-configholds the rest of~/.claude— sessions, project settings, MCP servers, plugins.
By default every project shares that one config volume too, so what you did in
one repo is visible in the next. --volume-per-project gives each workspace its
own, named after the directory plus a hash of its full path
(bclaude-config-myrepo-1a2b3c4d), so two repos called api don't collide and
the name doesn't change with how you spell the path. The auth volume is
unaffected either way: a new project is not a new login.
bclaude --volume-per-project # this repo gets its own sessions and settings
bclaude clean --list # what exists, for which project
bclaude clean --prune # drop volumes whose project directory is goneClaude Code only knows about one config directory, so the entrypoint copies the login in from the auth volume at startup and writes it back when the session ends — the token gets refreshed while you work, and that refresh has to reach your other projects. It writes back only a token this session actually changed, and stands aside if a session that started alongside it refreshed later, so running several at once won't put a spent token over a live one.
Volumes bclaude creates are labelled, which is how --list and --prune know
what they're looking at. --prune only ever considers per-project volumes whose
recorded project directory is gone — never the shared config or auth volume.
Isolation caveat: separate config volumes separate state, not trust. Every project's container mounts the same auth volume, so anything that runs in one can read the login.
Your host token stays on your host. bclaude does not copy
~/.claude/.credentials.json anywhere: the container logs in on its own the
first time you run it, and that login is stored in the bclaude-auth volume —
a one-time step for all your projects.
| How to be logged in | What it costs you |
|---|---|
| Log in inside the container (default) | one login prompt, ever; the host token is never exposed |
ANTHROPIC_API_KEY |
forwarded into the container when set; no OAuth token involved |
--seed-creds |
copies your host login into the auth volume — convenient, but the host refresh token is now in there too |
--seed-creds mounts the host file read-only and the entrypoint copies it in,
replacing any login already there — which makes it the fix for a seeded token
that went stale, too. Once credentials exist the entrypoint sets
hasCompletedOnboarding: true, so interactive launches skip the onboarding flow.
Caveats: whichever way you log in, the token ends up in the auth volume, which
every project's container mounts and anything running as the container user can
read (same as on your host); and with --seed-creds host and container hold
independent copies of the refresh token, so a rotation may force one side to
re-auth. If untrusted code ran in the container, rotate by re-logging in on
the host.
bclaude --allow-pkg
# inside: sudo apt-get update && sudo apt-get install -y <pkg>apt-get update is required first (the image ships empty lists), and installs
don't persist (--rm) — for anything permanent, edit the embedded
Containerfile in the script (bclaude show containerfile to see it) and run
bclaude build. --allow-pkg relaxes no-new-privileges + cap-drop=ALL;
sudo is scoped to apt-get/apt/dpkg in /etc/sudoers.d/claude-apt.
- keep-id: correct
/workownership, but an escape lands as your host user rather than a throwaway subuid. - Unrestricted egress: the container can reach your LAN and host services
(e.g. Postgres:5432). Claude needs the API, so a useful restriction would be
an allowlist proxy that permits only
api.anthropic.com, but this is not there, yet.
tests/run-tests.sh --fast # ~0.5s, no podman needed: parsing, flags, arg routing
tests/run-tests.sh # also builds the image and runs the containerSafe to run on a machine you actually use bclaude on: the suite clears every
BCLAUDE_* variable from your environment and works only on
localhost/bclaude-test:latest, bclaude-test-config and bclaude-test-auth,
which it removes when it finishes (BCLAUDE_KEEP=1 keeps them). It never reads
your host credentials and never writes outside those volumes, tests/ and
mktemp directories. The full run does build an image (a few minutes, needs
network) and start containers.
CI (.github/workflows/ci.yml) runs shellcheck, the fast suite, and the full
suite with a real podman build on every push.
- Build-time
can't raise ambient capabilitywarnings are normal in rootless mode. bclaude doctortells you exactly what's missing on a fresh machine (podman,uidmap,/etc/subuidentries, cgroup delegation).
MIT — see LICENSE.