bulkhead is a small Rust CLI for running local coding agents inside a hardened devcontainer.
The bundled template currently reflects a Rust-heavy maintainer workflow: rustup, zellij, vim, GitHub CLI, and a few audit-oriented terminal tools are available by default.
The basic model is simple:
- the current project directory is writable inside the container
- the rest of your laptop is not exposed unless you explicitly add mounts
.devcontaineris mounted read-only inside the container so code running in-container cannot rewrite the host-executed container config during rebuildbulkhead.tomlis mounted read-only inside the container so code running in-container cannot rewrite its own host-side policy
This is still under active development. The config format and CLI are not treated as stable yet.
Current design notes:
Local agent workflows are useful, but running them directly on the host with broad access is a bad default. bulkhead aims to make the safe path easy:
- generate a devcontainer workspace
- keep host exposure narrow by default
- manage extra mounts explicitly
- make common lifecycle operations simple from one CLI
The current default remains the simplest flow:
bulkhead shellGit-aware isolation support:
- clone mode for stronger isolation under
.bulkhead/clones/ - Git worktrees inside those isolated clones when you want lighter-weight parallel checkouts
Prerequisites:
- a Docker runtime such as Docker Desktop, OrbStack, or Colima
- the Dev Container CLI
Install with Homebrew:
brew tap pmembrey/bulkhead
brew install bulkheadOr build and install the binary from this repo:
cargo install --path .Create a workspace:
mkdir my-project
cd my-project
bulkhead shellCreate and enter an isolated clone from an existing repository:
cd my-repo
bulkhead clone shell feature-x --createIf bulkhead.toml does not exist yet, bulkhead will offer to create it and let you choose a preset. If the Dev Container CLI is missing, use:
bulkhead doctor --fixThe normal entrypoint is:
bulkhead shellThat will bootstrap the workspace if needed, start the container if needed, and open a bash shell inside it.
Other useful commands:
bulkhead upStart or ensure the container is running without opening a shell.bulkhead rebuildRebuild the container after changingbulkhead.toml, mounts, or managed files.bulkhead downStop the running container but keep its managed resources.bulkhead statusShow workspace config, remote user, mount count, and current container state.bulkhead logsShow Docker logs for the workspace container.bulkhead logs --tail 200 -fFollow recent container logs live.bulkhead exec -- pwdRun a one-off command inside the container without opening an interactive shell.bulkhead clone shell feature-xRe-enter an existing Bulkhead-managed isolated clone by name.bulkhead clone shell feature-x --createCreate.bulkhead/clones/feature-x, bootstrap Bulkhead there when safe, and open a shell inside it.bulkhead clone shell review-fix --create --branch fix/reviewUse a simple managed clone name on disk but create a different branch inside the clone.bulkhead clone listShow the managed isolated clones for the current repository.bulkhead clone remove feature-xDelete one managed clone without touching the source checkout.bulkhead mount listShow the extra host path mounts currently configured inbulkhead.toml.bulkhead mount add ~/drop /drop --rwAdd a writable host mount.bulkhead mount add ~/secrets /secrets --access roAdd a read-only host mount.bulkhead mount remove /dropRemove a configured host mount by source or target.bulkhead config git statusShow whether the managed host~/.gitconfigmount is enabled.bulkhead config git disableDisable the managed host~/.gitconfigmount.bulkhead destroyRemove the container and Bulkhead-managed Docker resources for the workspace.
bulkhead.toml is the source of truth. bulkhead generates .devcontainer/devcontainer.json from it.
Example:
name = "Bulkhead Agent Sandbox"
workspace_folder = "/workspace"
remote_user = "developer"
agents = ["claude", "codex", "pi"]
[build]
dockerfile = ".devcontainer/Dockerfile"
context = ".devcontainer"
[git]
enabled = true
[[path]]
source = "~/drop"
target = "/drop"
access = "rw"A few important points:
remote_useris set from the host username when a template is created- the bundled Dockerfile makes
remote_userthe actual non-root account in the container, not just the exec target [build]points at the Dockerfile and build context to use, relative to the workspace root- the bundled Dockerfile is Rust-oriented by default, pins its base image and bundled tool versions, and keeps the base devcontainer bash setup intact, but you can point
[build]at another Dockerfile in your repo if your workflow is different - if you replace the bundled Dockerfile, your custom build is responsible for creating whatever
remote_useryou configure featuresis allowlisted to Bulkhead-supported Dev Container Features, because features can carry their own runtime metadataagentspreinstalls pinned supported agent CLIs inside the container and attaches persistent config volumes for them- currently supported agents are
claude,codex, andpi claudeforwardsANTHROPIC_API_KEYandCLAUDE_CODE_OAUTH_TOKENfrom the host and persists config under~/.claudein the container- if
CLAUDE_CODE_OAUTH_TOKENis set, Bulkhead also tries to seed Claude auth during post-create so headless setup can avoid the browser login flow - if neither
ANTHROPIC_API_KEYnorCLAUDE_CODE_OAUTH_TOKENis set, expect a one-time Claude login inside the container; the persisted~/.claudevolume keeps that auth state across rebuilds codexforwardsOPENAI_API_KEYfrom the host and persists config under~/.codexin the containerpiforwardsOPENAI_API_KEYandANTHROPIC_API_KEY, persists config under~/.pi, and installs@mariozechner/pi-coding-agentafter bootstrapping pinnednvmand Node versions during post-create[git]is a dedicated managed feature for mounting host~/.gitconfigread-only into the container user's home- extra host paths live under
[[path]] accessdefaults to read-only unless you explicitly request write access[[path]]sources must resolve to plain host paths; variable-based sources such as${localEnv:...}are not allowed- mount targets are normalized and may not point at or under Bulkhead's read-only
/workspace/.devcontaineror/workspace/bulkhead.tomlmounts run_argsis allowed for narrow Docker options, but Bulkhead rejects flags that would bypass its host-access policy, including privileged mode, host bind mounts, devices, host namespaces, Docker security options, and any--cap-addvalue outside theNET_ADMIN/NET_RAWallowlist
Clone mode is the recommended Git-aware isolation workflow:
bulkhead clone shell feature-x --createThat command:
- creates an independent local clone under
.bulkhead/clones/feature-xwhen it does not exist yet - bootstraps Bulkhead there when safe
- starts the normal Bulkhead shell flow inside that clone
- keeps the source checkout and source repository metadata out of the container
- warns if the source repository is dirty, since clone mode starts from committed Git state only
Useful workflows:
# Re-enter an existing managed clone
bulkhead clone shell feature-x
# Create a fresh isolated clone and branch from origin/main
bulkhead clone shell feature-x --create --base origin/main
# Create a detached scratch clone
bulkhead clone shell scratch --create --detach
# See what managed clones already exist
bulkhead clone list
# Remove one managed clone
bulkhead clone remove feature-xManaged clone names are simple directory names under .bulkhead/clones/. If you
want a different branch name inside the clone, set it explicitly:
bulkhead clone shell review-fix --create --branch fix/reviewThe clone name is also the default Git branch name. If the on-disk clone name is
not a valid Git branch name, pass --branch <name> or --detach.
Because clone mode uses a normal independent Git clone, you can still create Git worktrees inside the clone later if you want them:
bulkhead clone shell feature-x --create
# inside the clone:
git worktree add ../feature-x-scratchbulkhead is trying to give you a practical host-protection boundary, not perfect sandboxing.
Defaults:
- current workspace mounted read-write
.devcontainermounted read-onlybulkhead.tomlmounted read-only- no Docker socket mount
- dangerous Docker runtime flags rejected, including privileged mode, host bind mounts, devices, host namespaces, Docker security options, and any
--cap-addvalue outside theNET_ADMIN/NET_RAWallowlist - minimal host mounts unless explicitly configured
Still true:
- code inside the container can fully modify the repo you launched from
- network access is not blocked by default
- adding broad writable host mounts weakens the model
Clone mode changes that tradeoff for Git-based workflows by moving the
container into a Bulkhead-managed isolated clone under .bulkhead/clones/.
That design is documented in docs/clone-mode.md.
bulkhead doctor checks:
- Docker installed
- Docker daemon reachable
- Dev Container CLI installed
- Docker buildx health
It also tries to surface the common Docker buildx permission problem early, including the ~/.docker/buildx/activity/... operation not permitted failure that can otherwise show up later during devcontainer up.
bulkhead is heavily inspired by Trail of Bits' claude-code-devcontainer project:
This repo started by studying and borrowing the security posture of that project, then rebuilding the operator layer as an agent-agnostic Rust CLI instead of a Claude-specific Bash wrapper.
This project was developed with assistance from AI coding and review tools. All code, design decisions, and releases are reviewed and approved by the maintainer(s), who remain responsible for the software.
Licensed under either of:
- MIT license (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option.