A zero-config Docker sandbox that boots straight into Claude Code — pre-loaded with the exact toolchain your build needs, so you just vibe and ship.
Get cooking · Why it slaps · Loadouts · Lock it in a VM
Neovim + LSP, tmux and zsh under the hood — see it below.
You know the move: let the agent run wild and actually get stuff done — but
not on your real machine. So give it its own. One command drops Claude into a
disposable container with --dangerously-skip-permissions fully on, a real IDE
under the hood (Neovim + LSP, tmux, zsh), and a profile that's already got
the entire toolkit for what you're building. Laptop stays pristine. Box gets
nuked on exit. You just talk.
No build step, no setup ritual — install the wrappers once, then they pull prebuilt images from Docker Hub on demand.
Install — clone to ~/.dev and drop the wrappers on your PATH:
git clone https://github.com/nemanjan00/dev-environment ~/.dev
export PATH=$PATH:~/.dev/bin
echo "export PATH=\$PATH:$HOME/.dev/bin" >> ~/.zshrc
echo "export PATH=\$PATH:$HOME/.dev/bin" >> ~/.bashrcRun — from any project directory:
claude login # once (or export ANTHROPIC_API_KEY=sk-...)
claude-docker # sandboxed Claude, dropped into your project
claude-docker --profile ctf # ...packing a full binary-exploitation kitYour project's mounted, your auth + git identity tag along, and Claude is live in a throwaway box. Close it and it's gone.
The wrappers self-update on launch (
git pull --ff-onlyon~/.dev) so you always run the latest profiles and tooling — setDEV_NO_UPDATE=1to skip it. This fetches and re-execs code from the install clone's git remote on the host before any sandbox exists, so keep~/.devout of any sandbox mount (never--mount ~/.dev): an agent that could rewrite the launcher there would have it run on your host at the next launch.
- 🧰 Loadouts, not setup. Ten ready-made profiles —
reversing,android,embedded,maker,analyst,librarian,presenter,scraper,ctf, or plaindefault. Each stacks on one shared base, so swapping toolchains is a single--profileflag, not an afternoon. - 🦙 Not just Claude. opencode rides along in the same box as a
drop-in alternative agent (
opencode-docker/opencode-vm) — anddev-ollamapoints it at a model served by your host's Ollama, local or cloud, so you can swap the brain without rebuilding the box. - 🔒 Send it — safely. Claude runs unleashed because it's sandboxed; your
host never feels it. Want it spinning up its own containers too?
claude-vmwraps the whole thing in a throwaway VM. - 🧠 The box explains itself. Every image ships a
/work/CLAUDE.mdand each profile appends its own playbook — some even auto-load Claude skills (thectfprofile hands Claude a ready-to-go/pwnworkflow). Less prompting, more shipping. - ⚡ Zero-config, zero residue.
claude loginonce and the wrapper handles auth, config, and git. No toolchains pollute your machine, and the container itself is--rm'd on exit — the only host writes are your project and Claude's own config (so sessions and memory carry over). - 🖥️ Slaps without Claude too. It's a legit portable IDE — Neovim + LSP, tmux, zsh, asdf Node/Python — even with the AI switched off.
Three moving parts, zero ceremony:
claude-dockerruns the image, mounts your current directory at/work/project, and copies in your Claude auth + git config. It launchesclaude --dangerously-skip-permissionsinside tmux — pulling the latest image first, so you're always current.- The image is Arch-based: a shared
baselayer (Neovim, tmux, zsh, asdf Node/Python, Claude Code) plus a thin profile layer that adds the domain tools. Each profile also appends its own docs to/work/CLAUDE.md, so Claude knows what's installed and how to drive it. - Nothing leaks. No toolchains land on your host, and the container is
--rm'd on exit — the only things written back are your project and Claude's own config dir (so your sessions and memory persist). Need harder isolation — say, Docker-in-Docker —claude-vmruns the same image inside an ephemeral Vagrant/libvirt VM that's destroyed on exit.
- Get cooking
- Why it slaps
- How it works
- Build it
- Profiles
- Run it
- Opening project inside of it
- Claude Code
- The generic runner (
dev-docker/dev-vm) - opencode
- Per-project sandbox layout (
.dev/config.json) - VM isolation
- Components
- Supported languages
- Author
# Build base image
docker build -t nemanjan00/dev:base .
# Build a profile (default, reversing, etc.)
docker build -t nemanjan00/dev:default profiles/default/
docker build -t nemanjan00/dev:reversing profiles/reversing/
docker build -t nemanjan00/dev:embedded profiles/embedded/
docker build -t nemanjan00/dev:android profiles/android/
docker build -t nemanjan00/dev:maker profiles/maker/
docker build -t nemanjan00/dev:analyst profiles/analyst/
docker build -t nemanjan00/dev:librarian profiles/librarian/
docker build -t nemanjan00/dev:presenter profiles/presenter/
docker build -t nemanjan00/dev:scraper profiles/scraper/
docker build -t nemanjan00/dev:ctf profiles/ctf/
docker build -t nemanjan00/dev:emulation profiles/emulation/
docker build -t nemanjan00/dev:multimedia profiles/multimedia/
# With custom UID/GID (to match your host user) — apply to the base image
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t nemanjan00/dev:base .Think of a profile as a loadout. A shared nemanjan00/dev:base layer carries the common kit (zsh, Neovim, tmux, Node.js, Python, Claude Code); each profile stacks the domain tools on top. Because they share that base, switching loadout is one --profile flag and a small incremental pull — never a fresh multi-gigabyte download.
| Profile | Tag | Description |
|---|---|---|
default |
nemanjan00/dev:default |
Base environment, no extras |
reversing |
nemanjan00/dev:reversing |
Reverse engineering & forensics: radare2, r2ghidra, r2mcp, jadx, binwalk, apktool, adb, volatility3, unicorn, keystone, magika, wireshark-cli, foremost |
embedded |
nemanjan00/dev:embedded |
Embedded development: arm-none-eabi toolchain, platformio, avrdude, esptool, openocd, stlink, sigrok-cli, flashrom |
android |
nemanjan00/dev:android |
Android / LineageOS builds: repo, git-lfs, JDK 17/11, android-tools, ccache, multilib libs, AOSP host toolchain |
maker |
nemanjan00/dev:maker |
Physical-world maker: OpenSCAD for 3D-printable parts, bun + pre-installed tscircuit CLI for PCB design |
analyst |
nemanjan00/dev:analyst |
Data / infra analyst (extends reversing): aws-cli, s3cmd, rclone, psql, mariadb, sqlite, duckdb, dsq, mongosh, valkey-cli, rabbitmq admin, lnav, httpie, websocat, yq, protoc, dig, ffmpeg |
librarian |
nemanjan00/dev:librarian |
Document & ebook reading: pandoc, poppler (pdftotext), mupdf-tools, qpdf, pdfgrep, catdoc, djvulibre, unrtf, tesseract OCR, glow, w3m |
presenter |
nemanjan00/dev:presenter |
Slide decks from Markdown via pandoc → beamer → xelatex: pandoc-cli, texlive (xetex, latexextra, fontsextra, pictures), fontconfig, Hack Nerd Font |
scraper |
nemanjan00/dev:scraper |
Web scraping against anti-bot sites: CloakBrowser (stealth Chromium, Playwright/Puppeteer drop-in), Xvfb for headed mode, Chromium runtime libs, full font set |
ctf |
nemanjan00/dev:ctf |
Binary exploitation / CTF (extends reversing): pwntools, GEF, ROPgadget, one_gadget, seccomp-tools, patchelf — plus an auto-loaded pwn exploitation skill |
emulation |
nemanjan00/dev:emulation |
Run/boot foreign-arch binaries & firmware (extends reversing): qemu-emulators-full (system & user-mode emulation for all arches — qemu-system-arm/mips/…, qemu-user), edk2 OVMF/AAVMF UEFI firmware, dosfstools/mtools for ESP images |
multimedia |
nemanjan00/dev:multimedia |
Audio/video/image manipulation: ffmpeg, imagemagick, graphicsmagick, sox, libwebp, libavif, mediainfo, exiftool, jpegoptim/optipng/gifsicle, potrace, ghostscript |
To use a profile with the CLI scripts:
claude-docker --profile reversing
claude-vm --profile reversingAdd a directory under profiles/ with a Dockerfile that extends the base image:
FROM nemanjan00/dev:base
USER 0
RUN pacman -Syu --noconfirm your-packages-here
USER 1000CI builds and pushes every profile from .github/workflows/build.yml: base-derived profiles go in the build matrix, while a profile that extends another profile gets its own job chained with needs: (see ctf/analyst). Add your profile there so it ships.
If the profile needs extra bind mounts or env vars at runtime (e.g. a persistent ccache for Android builds), add an executable docker-args.sh in the profile directory. claude-docker runs it when the profile is selected and appends its stdout to the docker run arguments:
#!/bin/bash
# profiles/myprofile/docker-args.sh
mkdir -p "$HOME/.cache/myprofile"
echo "-v $HOME/.cache/myprofile:/work/.cache/myprofile"Skip the wrapper and you've still got a self-contained IDE — Neovim, tmux, and zsh in a throwaway container:
docker run -ti nemanjan00/dev:defaultMount your code at /work/project and drop straight into a tmux session on it:
docker run -ti -eTERM=xterm-256color -v$(pwd):/work/project nemanjan00/dev:default zsh -ic "cd project ; tmux"Claude Code is pre-installed. The quickest way to get started is with the CLI scripts:
# Direct Docker — simple, no VM overhead
claude-docker
# VM-isolated — Claude gets its own Docker daemon, fully sandboxed
claude-vmBoth scripts auto-detect and mount ~/.claude.json (OAuth), ~/.claude/ (config), and ~/.gitconfig into the container. Claude runs with --dangerously-skip-permissions since the environment is sandboxed.
# API key (works with both scripts)
ANTHROPIC_API_KEY=sk-... claude-docker
# OAuth — just run `claude login` on the host first, then:
claude-docker # ~/.claude.json is mounted automatically| Host path | Container path | Purpose |
|---|---|---|
~/.claude.json |
/work/.claude.json |
OAuth credentials (from claude login) |
~/.claude |
/work/.claude |
Full Claude config (settings, memory, CLAUDE.md) |
~/.gitconfig |
/work/.gitconfig |
Git identity and settings (copied in; host file untouched) |
Trust boundary. The sandbox is for keeping the agent off your host filesystem — it is not a vault for the credentials you hand it.
~/.claudeand~/.claude.jsonare mounted read-write so config and memory persist, which means the unleashed agent can read everything in them (OAuth tokens, any MCP secrets, every project's memory) and edit~/.claude/settings.json— whose hooks run on your host the next time you launch Claude outside the sandbox. The launcher neutralizes one escalation automatically (it refuses to persist an agent-injected globalmcpServersback to~/.claude.json), but treat anything reachable from~/.claudeas visible to whatever you run. Prefer a scopedANTHROPIC_API_KEYover mounting host OAuth if that matters to you.
The container ships with a /work/CLAUDE.md that documents the environment for Claude. Profile images append profile-specific tool documentation to it. Since Claude Code walks up from the project directory, /work/CLAUDE.md is always loaded as an ancestor of /work/project/. Your project can still have its own CLAUDE.md — both will be read.
Frontend developers who need container ports (e.g. dev servers) accessible on the host can enable host networking:
claude-docker --host-networkThis passes --network host to Docker, so any ports the container listens on are directly available on localhost.
Heads up: host networking also removes the network boundary in the other direction — the unleashed agent can now reach every service bound to your host's loopback/LAN (localhost-only databases, admin UIs, metadata endpoints). For the dev-server use case, publishing specific ports (
-p 3000:3000via adocker-args.shor--mount-style flag) is tighter than full host networking.
# Minimal
docker run -ti -e ANTHROPIC_API_KEY -v$(pwd):/work/project nemanjan00/dev:default zsh -ic "cd project ; claude"
# Full setup
docker run -ti -e ANTHROPIC_API_KEY \
-v$(pwd):/work/project \
-v~/.claude:/work/.claude \
-v~/.claude.json:/work/.claude.json \
-v~/.gitconfig:/work/.gitconfig:ro \
nemanjan00/dev:default zsh -ic "cd project ; claude"claude-docker and claude-vm are thin shims over two generic launchers,
dev-docker and dev-vm, that run any command in the sandbox and let you
choose what to mount. claude-docker is exactly dev-docker --claude, so its
behavior is unchanged — the generic form just exposes more.
dev-docker # bare tmux shell in the sandbox
dev-docker --claude [claude args...] # Claude Code (same as claude-docker)
dev-docker --opencode [opencode args] # opencode (see below)
dev-docker --mount ~/.npmrc:/work/.npmrc -- npm run lint # run a one-off command| Option | Meaning |
|---|---|
--claude / --opencode |
Preset: mount that agent's auth and launch it. Trailing args pass through. |
--mount SRC[:DST][:ro] |
Extra bind mount (repeatable). ~ allowed; no DST → same path; :ro → read-only. |
--home DIR |
Read Claude config (.claude / .claude.json) from DIR instead of $HOME. |
--ollama |
opencode only: point it at the host's Ollama. |
--profile NAME |
Image profile (default default). |
--host-network |
Share the host network. |
-- CMD... |
Run an arbitrary command instead of a preset/shell. |
dev-vm mirrors this, except arbitrary --mount is docker-only (the VM only
syncs the project); read-only project carve-outs from .dev/config.json still
apply.
The image ships opencode alongside Claude Code.
opencode-docker # == dev-docker --opencode
opencode-vm # == dev-vm --opencode
opencode-docker --ollama # use a model served by the host's Ollamaopencode reads ANTHROPIC_API_KEY like Claude does, and its auth/state
(~/.config/opencode, ~/.local/share/opencode) are mounted from the host so
logins persist across throwaway containers.
opencode can use models served by your host's Ollama. The quickest way is
dev-ollama, which takes the model and wires everything up:
dev-ollama opencode --model kimi-k2.7-code:cloud
dev-ollama opencode --model qwen2.5-coder --profile reversingIt generates a one-off provider config registering that model and launches
dev-docker --opencode --ollama. (Claude + Ollama isn't supported yet — Claude
Code speaks only the Anthropic API, so it would need a translation router we
don't bundle; dev-ollama claude … errors with that explanation.)
For the baked default models without dev-ollama, use --ollama directly:
opencode-docker --ollama # uses the models listed in /work/opencode-ollama.json--ollama selects the baked provider config (/work/opencode-ollama.json,
pointing at http://host.docker.internal:11434/v1); edit its models list for
the models you've pulled. Under dev-docker the container reaches your host's
Ollama directly. Under dev-vm, host.docker.internal resolves to the VM,
not your real host — so Ollama must be reachable from inside the VM.
Drop an optional .dev/config.json in a project to declare extra mounts and
read-only carve-outs. Absent file → default behavior, so existing projects are
unaffected. Parsed with jq on the host (ignored if jq is missing).
{
"mounts": [
{ "path": "~/work/shared-lib", "at": "/work/shared-lib", "mode": "ro" }
],
"project": {
"readonly": ["tests", "migrations"]
}
}mounts— extra paths to bring in (modedefaults torw). Docker-only;dev-vmwarns and skips them.project.readonly— sub-directories of this project to mount read-only. Use it to stop the agent editing tests to make them pass: it physically cannot write there. The whole.dev/directory is itself locked read-only so the agent can't relax its own sandbox for the next launch.
macOS: bind-mount permissions hold for directories only, not individual files. Read-only carve-outs are therefore directory-granular, and the
.dev/self-lock relies on.dev/being a directory (which is why the config lives at.dev/config.jsonrather than a bare dotfile).
For full isolation (e.g. giving Claude access to Docker), use claude-vm (or
dev-vm) to run the dev container inside a lightweight VM via Vagrant + libvirt. Each invocation creates an ephemeral VM that is destroyed on exit.
# Arch Linux
pacman -S vagrant libvirt qemu-full qemu-img
vagrant plugin install vagrant-libvirt# Run from your project directory — it gets mounted into the VM
cd /path/to/project
claude-vm
# With API key
ANTHROPIC_API_KEY=sk-... claude-vmBy default, claude-vm auto-detects ~/.claude.json, ~/.claude/, and ~/.gitconfig. You can override with env vars:
| Env var | Default | Purpose |
|---|---|---|
PROJECT_DIR |
$(pwd) |
Project directory to mount |
CLAUDE_CONFIG_DIR |
~/.claude |
Claude config (settings, memory) |
CLAUDE_AUTH |
~/.claude.json |
OAuth credentials file |
ANTHROPIC_API_KEY |
(none) | API key authentication |
VM_MEMORY |
16384 |
VM RAM in MB |
VM_CPUS |
2 |
VM virtual CPU count |
VMs are ephemeral — each claude-vm invocation gets a unique VM ID and cleans up on exit (including Ctrl-C). Multiple instances can run in parallel.
The container inside the VM has access to the VM's Docker socket (with correct group permissions via --group-add), so Claude can spin up additional containers as needed, fully isolated from the host.
Host (your machine)
└── Vagrant/libvirt VM (Alpine Linux, 16GB RAM, 2 vCPUs) ← VM_MEMORY / VM_CPUS
├── Docker daemon
└── dev container (this image)
├── Claude Code (--dangerously-skip-permissions)
├── Docker CLI → VM's Docker socket
├── Neovim, tmux, zsh
└── Project files (virtiofs mount)
Project files are mounted into the VM via virtiofs (native libvirt filesystem passthrough), so changes are reflected in both directions. The dev container cannot affect the host.
If vagrant up fails with dnsmasq: failed to create listening socket ... Address already in use, you have something bound on port 53 that conflicts with libvirt's DHCP. Run sudo claude-vm-setup to create a custom network with DNS disabled.
The base image — what every profile and the standalone IDE are built on:
- Neovim with my config and coc.nvim for LSP
- zsh with zplug and my config
- tmux with gpakosz/.tmux
- Claude Code and opencode coding agents
- asdf version manager (Node.js, Python pre-installed)
- fzf fuzzy finder
- ripgrep fast search
- jq JSON processor
- ctags code indexing
- CSS
- Dockerfile
- HTML (with emmet support)
- JS (eslint and tsserver)
- JSON
- PHP
- Python
- Bash
- SQL
- VimL
- XML
- YAML
- Much more (via coc.nvim extensions)
Install it, then from any project directory:
claude-docker --profile ctf # or reversing, android, maker, analyst, ...Sandboxed Claude, fully loaded, gone on exit. If it spared your real machine a bad day, drop a ⭐ — it's free.
