@hrm-shell-app/coco
v0.1.0
Published
Claude Code devcontainer launcher and template
Maintainers
Keywords
Readme
___
/ \
/=*=*=\
____/=======\____
/~~~~~~~~~~~~~~~~\
'-o-o-o-o-o-o-o-o-'coco
Claude Code devcontainer launcher and template.
TL;DR
A shareable devcontainer for running Claude Code in a sandboxed Linux container with an egress firewall, plus a small CLI to drive it:
npm i -g @hrm-shell-app/coco
cd ~/some-project
coco init # drop .devcontainer/ into the project
coco --up && coco # build, start, attach
coco allow api.x.com # punch a hole in the firewall, no restart
coco shadow my-app # isolate my-app/node_modules from the host
coco kill # tear it downWhy you'd want it: bind-mounts ~/.claude and ~/.ssh so Claude Code and
git work out of the box, locks outbound traffic to a known allowlist, and
gives every colleague the same container without sharing dotfiles. Adding
new firewall domains and isolating per-project node_modules no longer
requires a rebuild-loop.
Install
npm i -g @hrm-shell-app/cocoThis also installs @devcontainers/cli as a dependency. Docker must be running.
Use
cd ~/some-project
coco init # writes .devcontainer/ into the current directory
coco --up # build/start the container
coco # attach (auto-starts if not running)
coco --up --rebuild # rebuild from scratch
coco allow some-api.example.com # allow a new egress domain without restarting
coco kill # stop & remove this workspace's container
coco kill --all # stop & remove every devcontainer on the hostThe container is identified by the workspace folder it was started with. By
default that is the current directory; override with -w <path> or by setting
COCO_WORKSPACE.
Where to run coco init
The directory you run coco init in becomes the workspace: it gets
bind-mounted to /workspace/ inside the container, and that's the upper
bound on what Claude (and everything else in the container) can see and edit.
Two common patterns:
- One devcontainer for the whole machine —
coco initin~/Projects/(or wherever you keep all your repos). Claude can read and edit every subproject, which is great for cross-project work, refactors that span multiple repos, or having a single long-lived container. The trade-off is blast radius: Claude has reach into every project under that root. - One devcontainer per project —
coco initinside each project directory. Claude is scoped to just that project, which is safer and matches the "standard" devcontainer pattern, but you juggle one container per project and lose easy cross-repo access.
Pick based on how broad you want Claude's reach to be. You can also mix:
keep a shared ~/Projects/.devcontainer/ for general work and add a more
locked-down .devcontainer/ inside specific projects when you need it.
Node version
The container's Node version is controlled by the NODE_VERSION build arg
(default 24). Override it per-host by exporting COCO_NODE_VERSION before
coco --up --rebuild:
export COCO_NODE_VERSION=20
coco --up --rebuildNode 24 ships with npm 11, which tracks platform-specific optional deps in
the lockfile and avoids the esbuild/@swc/* mismatch you hit when one
side ran npm install on macOS and the other on Linux.
Shadowing node_modules
For projects where you don't want the host and container sharing
node_modules at all (cleanest fix for the cross-platform native binary
problem), shadow the directory with a Docker named volume:
coco shadow my-project # add a volume mount for my-project/node_modules
coco shadow my-project --remove
coco shadow # list active shadows
coco --up --rebuild # required to applycoco shadow edits the mounts array in .devcontainer/devcontainer.json
between the // coco-shadows-start / // coco-shadows-end markers. Volume
names are scoped to the workspace path so different colleagues' workspaces
don't collide. The host's node_modules/ for that project becomes invisible
to the container (and vice versa) — IDE features like "go to definition"
into deps will need a host-side npm install too.
Adding domains to the firewall
coco allow <domain> [<domain>...] resolves each domain and adds its IPs to
the live allowed-domains ipset inside the running container — no restart,
no terminal loss. The domain is also appended to
.devcontainer/allowed-domains.local (gitignored) so the next container
start re-applies it automatically.
You can also edit .devcontainer/allowed-domains.local by hand (one domain
per line, # for comments) — init-firewall.sh reads it on every start.
What's in the template
template/.devcontainer/ ships:
Dockerfile— Node 24 (configurable viaNODE_VERSIONbuild arg) + zsh + Claude Code, plus iptables/ipset for the egress firewall.devcontainer.json— bind-mounts~/.claudeand~/.ssh, persists shell history in a named volume, runs the firewall script on start.init-firewall.sh— locks outbound traffic to GitHub, the npm registry, the Anthropic API, the VS Code marketplace, and the Tempo API. Edit the domain list in this file if you need other endpoints reachable from the container.
Publishing
The package is published as @hrm-shell-app/coco. Source lives at
simployer/coco. To cut a release:
npm version patch # or minor / major
npm publish # uses publishConfig.access = "public"
git push --follow-tags