001: Cellblock Phase 1 — Containerized Agents via workmux
Goal
workmux add feature-x opens a tmux window where claude-code runs inside a
Docker container with the worktree mounted. SSH works. Nix flakes work. Config
controls what each project can access.
What We Build
- Docker image — Alpine + git + ssh + tools + fuse-overlayfs (~73MB)
cellblockCLI — bash prototype (Go rewrite planned), config →docker run- Config —
~/.config/cellblock/config.yaml, per-project permissions - Nix provider shim — pluggable nix base layer (linux-volume now, host-bind future)
- Per-project COW overlays — overlayfs for /nix isolation
- workmux integration —
agent: cellblock run claude-codein.workmux.yaml
Core Design: Copy-on-Write Isolation
Everything uses the same pattern: read-only shared base + per-project COW upper.
/nix — two-layer overlay
/nix (overlayfs merge inside container)
├── upper: cellblock-nix-<project> per-project COW (Docker volume, tiny deltas)
└── lower: <provider-dependent> shared nix store (read-only)
Providers (shim abstraction for future Apple Containers):
| Provider | Lower layer | When |
|---|---|---|
linux-volume | cellblock-nix-linux Docker volume | Now: Docker on macOS (host /nix has Mach-O) |
host-bind | Host /nix bind-mount | Future: Apple Containers or Linux hosts |
linux-volume: one-timecellblock nix initruns nix inside a Linux container to install agents into a shared Docker volume. All projects share it read-only.host-bind: direct bind-mount, zero copy. Works when container arch matches host.- The overlay upper is identical either way — only the base source changes.
~/.claude — copy-on-start
- Host
~/.claudemounted read-only at/opt/claude-config - Entrypoint copies to
/home/cellblock/.claude(writable, per-container) - Isolated, ephemeral — changes die with container
Networking — bridge (default)
- Default Docker bridge — containers isolated from each other
- Host services via
host.docker.internal - Per-project
network: hostoption
Files
cellblock/
plans/ # phase plans (001, 002, 003)
flake.nix # nix pkg + dev shell
bin/cellblock # CLI (bash prototype)
lib/common.sh # logging, path utils
lib/config.sh # YAML config reader (yq)
lib/resolve.sh # project/branch detection
lib/docker.sh # docker run arg assembly
lib/nix.sh # nix provider shim + overlay management
image/Dockerfile # Alpine + git + ssh + tools
image/entrypoint.sh # UID mapping, overlayfs, config copy
config/example.yaml # example config
tests/ # bats tests
CLI
cellblock run <agent> [-- args...] # run agent in container for cwd project
cellblock shell [project] # interactive shell in container
cellblock stop [project] # stop container
cellblock status # list running containers
cellblock nix init [packages...] # init shared Linux nix store (one-time)
cellblock nix status # list nix volumes
cellblock nix reset-project [proj] # reset per-project nix overlay
cellblock image build # build the docker image
Config
~/.config/cellblock/config.yaml:
defaults:
image: cellblock:latest
ssh_agent: true
nix_provider: linux-volume # "linux-volume" or "host-bind"
network: bridge # "bridge" or "host"
claude_config: ~/.claude
projects:
treehouse:
path: ~/work/treehouse
env: [MIX_ENV=dev]
petal_pro:
path: ~/work/petal_pro
env: [MIX_ENV=dev, DATABASE_URL]
mounts:
- ~/work/shared-libs:/shared-libs:ro
What cellblock run claude-code Executes
For cwd /Users/ijcd/work/treehouse, branch main, provider linux-volume:
docker run --rm -it \
--name cellblock-treehouse-main-claude-code \
--label cellblock=true \
--label cellblock.project=treehouse \
--cap-add SYS_ADMIN \
-v /Users/ijcd/work/treehouse:/workspace \
-v cellblock-nix-linux:/nix-base:ro \
-v cellblock-nix-treehouse:/nix-overlay \
-v /Users/ijcd/.claude:/opt/claude-config:ro \
-v /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock \
-e SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock \
-e USER_UID=$(id -u) -e USER_GID=$(id -g) \
-e MIX_ENV=dev \
cellblock:latest \
/nix/var/nix/profiles/default/bin/claude-code
Build Order
- ✅ image/Dockerfile + entrypoint.sh
- ✅ lib/common.sh + lib/config.sh
- ✅ lib/resolve.sh
- ✅ lib/docker.sh
- ✅ bin/cellblock — CLI wiring
- ✅ lib/nix.sh — provider shim + overlay management
- ✅ flake.nix
- 🔄 Integration testing (shell, overlay verified; nix init next)
- ⬜ workmux integration — end-to-end test
Verification
- ✅
cellblock image buildsucceeds - ✅
cellblock shell→git --versionworks inside container - ✅ Overlay mount confirmed:
overlay on /nix type overlay (rw,...) - ✅
~/.claudecopy-on-start works, isolated per container - ✅ SSH agent socket mounted and accessible
- ⬜
cellblock nix initpopulates Linux nix store - ⬜
cellblock run claude-codestarts agent in container - ⬜ Inside:
ssh -T git@github.comsucceeds - ⬜ Inside:
nix developworks on a flake project - ⬜
workmux add test-feature→ tmux window with containerized agent - ✅ 36 bats tests passing
Not in Phase 1
- Pluggable isolators / Apple Containers / remote (Docker only, but shim ready; see plan 004)
- Remote isolators: Fly.io, E2B, cloud-vm (see plan 004)
- Treehouse network identity
- Multi-project
cellblock up - Orchestrator tier
- Proxy tracking
- Go rewrite (planned after API stabilizes)
- Watchdog interceptor (see plan 002)
- LiveView workmux UI (see plan 003)
Resolved
- ~/.claude isolation — copy-on-start. Mount ro, copy in entrypoint. Ephemeral.
- Networking — bridge default.
host.docker.internalfor host access. - Nix architecture mismatch — host /nix has macOS binaries, containers are Linux.
Solved with provider shim:
linux-volume(shared Linux nix Docker volume) for now,host-bind(direct mount) when Apple Containers make arch match. - Nix isolation — two-layer overlayfs. Shared base (ro) + per-project COW upper.
Base protected. Per-project writes persist.
nix developworks.