Containerised remote development environment for an AI coding workstation.
One Docker container with its own unprivileged sshd, published only on the node's Tailscale address.
A herdr client on a laptop attaches to it as a saved machine and runs OMP and Claude Code
agents inside.
Setup · Connecting · Git identities · Secrets · CLI · Docker · Security
The container is the sandbox. Agents running with bypassed permissions reach the project tree, the internet and a rootless project Docker daemon - never the host filesystem, never the host's root Docker daemon, and never a private key: the box holds public keys only.
flowchart LR
L["laptop<br/>herdr client"] -->|" ssh devbox<br/>Tailnet only "| H["<workstation><br/>BIND_ADDR:2223"]
H -->|" DNAT "| C["container :2222<br/>sshd as dev"]
C --> P["panes: OMP, Claude Code, node, gh, wt"]
C --- V["/home/dev<br/>bind mount"]
Workstation side, from the laptop:
./bin/push <workstation> # sync the repo to ~/devbox
ssh <workstation> 'cd ~/devbox && ./bin/devbox env' # .env from .env.example, BIND_ADDR from Tailscale
ssh -t <workstation> 'cd ~/devbox && sudo ./bin/rootless-docker' # once: project Docker, asks for a password
ssh <workstation> 'cd ~/devbox && ./bin/devbox up && ./bin/devbox doctor'
herdr machine add devbox --label "Devbox" # once the ~/.ssh/config blocks below exist
ssh <workstation> 'cd ~/devbox && ./bin/devbox skills' # optional: agent skills + browser automation
./bin/sync-omp # optional: this laptop's OMP preset → devbox
ssh -t devbox claude # once, if you use Claude Code: /loginNot optional: the Devbox Laptop key in 1Password (only ~/.ssh/devbox.pub on disk), the two ~/.ssh/config
blocks, and a non-empty BIND_ADDR. All three are in Setup.
The laptop is the only place a private key or a 1Password session ever lives; the devbox borrows them per connection. Two scripts make the laptop match:
./bin/install-agent # omp + claude launchers, gh shim, credential helper, fence, agent gitconfigs - same as the devbox
./bin/laptop-doctor # acceptance test: keys, ~/.ssh/config, signing, the override, tokens, connections- Keys - one
id_<slug>.pub/signing_<slug>.pubpair per identity in~/.config/devbox/identities.conf, plusdevbox: 1Password SSH items, public halves only in~/.ssh, selected perHostbyIdentityFile <name>.pub+IdentitiesOnly. - Git - your own commits sign through
op-ssh-sign; agents run through theomp/claudelaunchers and get HTTPS remotes, per-operation tokens, a bot author and no signing, on the same clones. On the laptop the launchers need onePATHline, whichinstall-agentprints. - Tokens - one
GH_TOKEN_<SLUG>per identity in~/.config/devbox/secrets.env(mode 600) and each identity's GitHub App credentials in its ownappdirectory, read only by agent sessions.
laptop-doctor names what is missing; the devbox-laptop skill and
Git identities walk the fixes.
| Doc | Read it when |
|---|---|
| Setup | First deploy, .env reference, laptop key, ~/.ssh/config, laptop agent install |
| Connecting | Getting a shell: herdr panes, ssh devbox, Moshi, devbox shell |
| Git identities | Cloning repos, the identity registry, manual vs agent git, signing, laptop install |
| Toolchain | What is installed, versions, OMP, Claude Code, Moshi hooks, agent skills |
| Secrets | Box-wide vs per-project, per-identity gh tokens, GCP ADC, App creds |
| CLI reference | Every bin/devbox, bin/push, bin/sync-omp, bin/sync-identities, bin/install-agent and bin/laptop-doctor flag |
| Networking | Exposure model, why UFW cannot help, port forwarding |
| Docker | Project containers, the rootless daemon, devbox-ports |
| Operations | Redeploy, restart, backup, doctor, troubleshooting |
| Security model | Boundaries, trust assumptions, what an escaped agent reaches |
./bin/push <workstation> --up # deploy + start (keeps state; asks before killing SSH sessions)
ssh devbox # shell in the container
ssh -A devbox # + push, pull, sign as yourself (forwards the 1Password agent)
herdr # attach panes; they survive client exit
ssh -N -L 5173:localhost:5173 devbox # reach a dev server
ssh devbox 'cd projects/app && docker compose up -d && devbox-ports' # project containers on localhost
ssh <workstation> 'cd ~/devbox && ./bin/devbox doctor'
ssh <workstation> 'cd ~/devbox && ./bin/devbox sessions' # who is connected (a recreate kills them)
./bin/sync-omp # push this laptop's OMP preset into the devbox
./bin/install-agent # laptop-side: same agent git override as the devbox
./bin/laptop-doctor # laptop-side acceptance test: keys, configs, override, tokens
./bin/sync-identities # push this laptop's identity registry into the devbox.env.example the only per-host configuration (.env is gitignored, never pushed)
Dockerfile pinned toolchain; ends as USER dev
docker-compose.yml ${BIND_ADDR}:${DEVBOX_SSH_PORT}:2222 is the whole network boundary
bin/devbox host CLI: env, up, down, rebuild, bootstrap, skills, shell, sessions, logs, hook, keys, doctor
bin/rootless-docker host-side, one-time: provisions the rootless project Docker daemon
bin/push laptop-side rsync deploy
bin/sync-omp laptop-side OMP preset sync (~/.omp/agent/config.yml → devbox)
bin/sync-identities laptop-side identity registry sync (~/.config/devbox/identities.conf → devbox)
bin/install-agent laptop-side: installs the omp/claude launchers, gh shim, devbox-identities and agent git config
bin/laptop-doctor laptop-side: checks keys, ssh/git config, the registry, the override, tokens and connections
container/ entrypoint.sh (PID 1), bootstrap.sh (user setup), skills.sh (optional), sshd_config
home/ templates installed into /home/dev by bootstrap (and onto the
laptop by install-agent)
home/.config/devbox/identities.conf.example identity registry template - you copy it to ~/.config/devbox/
identities.conf; nothing seeds it for you
home/.local/libexec/devbox-identities the one reader of identities.conf: sourceable library and CLI
home/.config/devbox/git/agent.gitconfig.tpl template devbox-identities renders into the agent gitconfig
docs/ this documentation
.agents/skills/ agent skills: devbox-basics, devbox-setup, devbox-laptop, devbox-deploy
.claude/skills/ symlinks to .agents/skills/ - the one skills directory Claude Code reads
CLAUDE.md imports AGENTS.md for Claude Code
AGENTS.md holds the conventions (Claude Code reads it through CLAUDE.md). Four skills in
.agents/skills/ (mirrored into .claude/skills/ by symlink) route an agent through the
same material: devbox-basics (what it is, how it is isolated),
devbox-setup (first install, connection failures),
devbox-laptop (keys, configs, the agent override on the laptop),
devbox-deploy (shipping a change and applying it).