██████╗ ██╗ ██╗ ██╗ ██████╗██╗ ██╗██╗ ██╗ ██████╗ ██╗ ███████╗
██╔══██╗██║ ██║ ██║██╔════╝██║ ██╔╝██║ ██║██╔═══██╗██║ ██╔════╝
██████╔╝██║ ██║ ██║██║ █████╔╝ ███████║██║ ██║██║ █████╗
██╔══██╗██║ ╚██╗ ██╔╝██║ ██╔═██╗ ██╔══██║██║ ██║██║ ██╔══╝
██████╔╝███████╗╚████╔╝ ╚██████╗██║ ██╗██║ ██║╚██████╔╝███████╗███████╗
╚═════╝ ╚══════╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝
Declarative Docker Sandbox manager. One YAML config, one command — sandbox is up with your runtimes, packages, and AI agent.
Sandboxes isolate your development environment from your host. Compromised dependencies can't touch your filesystem or phone home unless you explicitly allowlist the domain. Good default against supply chain attacks.
blvckhole init # scaffold config
blvckhole start # build image, create sandbox
blvckhole agent # launch AI agent insideRequires Docker Desktop with Docker Sandboxes (sbx CLI).
Download a pre-built binary from GitHub Releases, extract it, and put it somewhere in your PATH.
Or build from source (needs Go 1.26+):
git clone https://github.com/neoighodaro/blvckhole.git
cd blvckhole
go build -o blvckhole .
./blvckhole install # copies to ~/.local/bin/
./blvckhole install -d /usr/local/bin # custom directory
./blvckhole install -s # symlink instead of copy (picks up rebuilds)
./blvckhole install -s -d /usr/local/bin # bothblvckhole init # scaffold config
blvckhole start # build + launch
blvckhole ssh # open a shell
blvckhole run "pnpm install" # execute a command in the sandbox
blvckhole agent # launch the agent (starts sandbox if needed)
blvckhole agent --rebuild # force image rebuild first
blvckhole stop # stop (state is kept)
blvckhole restart # tear down + recreate
blvckhole status # show sandbox state, ports, runtimes
blvckhole network allow elevenlabs.io # allow a domain on the running sandbox
blvckhole network allow elevenlabs.io --persist # ...and save it to blvckhole.yaml
blvckhole network deny ads.example.com # block a domain (runtime-only)
blvckhole network remove elevenlabs.io --persist # remove the rule and drop it from config
blvckhole handoff # run the cross-sandbox handoff broker (foreground)
blvckhole handoff -D # ...in the background; blvckhole handoff -K to stop itblvckhole init creates .config/blvckhole/blvckhole.yaml. Also discovered at blvckhole.yaml in the project root.
name: my-project # default: directory name
agent: claude-code # or codex, copilot, cursor, docker-agent, droid, gemini, kiro, opencode, shell
# Skip Dockerfile generation — use your own image instead
# template: docker.io/my-org/custom-template:v1
packages: # apt-get
- ripgrep
- lazygit
- eza
runtimes:
node: "24" # major version (NodeSource) or exact: "24.16.0"
pnpm: "11.1.3" # needs node
# bun: "latest"
# python: "3.12"
# go: "1.23"
# php: "8.4"
# rust: "stable"
php: # additional extensions (added to defaults)
extensions:
- sqlite3
- imagick
ports:
- 3000
- "8080:80"
env:
NODE_ENV: development
env_file: # loaded in order, later wins
- .env
- .env.sandbox
scripts:
on_create: # run once, right after the sandbox is created
- "composer install"
on_start: # run on every container start (survives stop/resume)
- "./scripts/db-bridge.sh" # bridges, daemons — things a restart would wipe
shell:
directory: /home/agent/project/src
aliases:
g: "git"
dc: "docker compose"
network: # allowlist — when set, only these domains are reachable
- "*.npmjs.org"
- "registry.yarnpkg.com"
- "api.github.com"
handoff: # cross-sandbox question/answer broker
enabled: true
# url: http://host.docker.internal:8787 # default
claude: # Claude Code-specific
theme: ./my-theme.json # Catppuccin Mocha by default
plugins:
marketplaces:
- anthropics/claude-plugins-official
install:
- superpowers@claude-plugins-official
settings:
alwaysThinkingEnabled: true
zellij: # Zellij terminal multiplexer
display_name: My Project # tab name when launching the agent
memory: | # injected into the agent's CLAUDE.md
Project-specific instructions here.Sandboxes use bash with a colored prompt, git branch display, and persistent history. Aliases for bat, eza, lazygit, and please (sudo last command) are available when those tools are installed. Add your own via shell.aliases.
scripts.on_create runs once, right after the sandbox is created (migrations, dependency installs) — run via blvckhole start. scripts.on_start runs at the start of every shell and agent session — including after a stop/resume — so use it for state that a restart wipes: port bridges, background daemons, socat tunnels. on_start commands are baked into the sandbox's per-session init hook (/etc/sandbox-persistent.sh) at build time, run in a subshell with output suppressed, so they must be idempotent. (on_start requires a generated Dockerfile; it has no effect with a custom template.)
The old top-level
startup:key is deprecated. It still works (it now aliasesscripts.on_create) but prints a warning — migrate toscripts.on_create.
If ~/.claude/skills exists on the host (or is a symlink), its contents are automatically copied into the sandbox at build time. No configuration needed.
Let agents in different sandboxes ask each other threaded questions. A small broker runs on the host; sandboxes reach it over host.docker.internal.
Run the broker on the host:
blvckhole handoff # foreground (stops when you close the terminal)
blvckhole handoff -D # run in the background (daemon)
blvckhole handoff -K # stop a background broker
blvckhole handoff -P 9000 # custom port (default 8787)Then enable it per project so the sandbox can reach the broker:
handoff:
enabled: true
# url: http://host.docker.internal:8787 # defaultWhen enabled, blvckhole start injects $BLVCKHOLE_SANDBOX (this sandbox's name) and $BLVCKHOLE_HANDOFF_URL into the sandbox, allowlists the broker port, and auto-installs the sandbox-handoff skill. Agents use it to open threads, post answers, and long-poll for incoming questions — GET /handoff/threads?...&wait=<seconds> blocks until one arrives (capped at 5 min) instead of busy-polling. A live web board for watching threads is served at the broker URL (/handoff).
Supported: claude-code, codex, copilot, cursor, docker-agent, droid, gemini, kiro, opencode, shell
| Runtime | Version format | Notes |
|---|---|---|
node |
"24" or "24.16.0" |
Major version uses NodeSource, exact downloads from nodejs.org |
pnpm |
"11.1.3" |
Requires node |
bun |
"latest" or "1.2.0" |
|
python |
"3.12" |
|
go |
"1.23" |
|
php |
"8.4" |
|
rust |
"stable" or "1.80" |
git clone https://github.com/neoighodaro/blvckhole.git
cd blvckhole
go build ./...
go test ./...cmd/ CLI commands (cobra)
internal/
config/ YAML config parsing and validation
embedded/ Embedded assets (Dockerfile template, bashrc, theme)
envfile/ .env file parser
kit/ Kit generator (spec.yaml + files for sbx)
runtime/ Runtime installers (node, pnpm, bun, python, go, php, rust)
sandbox/ Wrapper around the sbx CLI
template/ Dockerfile template renderer and builder
ui/ Terminal styling (lipgloss)
- Create
internal/runtime/<name>.goimplementing:
type Runtime interface {
Name() string
Validate(version string) error
RootBlock(version string) string // Dockerfile commands as root
AgentBlock(version string) string // Dockerfile commands as agent user
EnvBlock(version string) string // ENV directives
}- Register in
internal/runtime/runtime.go - Add to
validRuntimesininternal/config/config.go - Add a commented example in the starter config in
cmd/init.go