This container exists to run agents with bypassed permissions, where the worst case is a lost project tree, not a lost host. The threat model is an agent (or dependency) executing arbitrary code inside the devbox.
Related: Architecture Β· Secrets Β· Docker Β· Networking
| Boundary | Enforced by |
|---|---|
| No host filesystem access | Only ${DEVBOX_DATA_DIR} mounted, at /home/dev (see accepted limit 5) |
| No host root Docker daemon | /var/run/docker.sock not mounted; reachable daemon is rootless |
| No privilege escalation | user: ${HOST_UID}:${HOST_GID}, cap_drop: [ALL], no-new-privileges:true |
| No public network exposure | ${BIND_ADDR}:${DEVBOX_SSH_PORT}:2222 β Tailnet address only |
| No password auth | PubkeyAuthentication yes, PasswordAuthentication no, UsePAM no |
| No private keys at rest | Devbox holds no SSH private key; AllowAgentForwarding yes only lets ssh -A devbox borrow the laptop's forwarded 1Password agent for one connection |
The image ends as USER dev; sshd runs unprivileged, only ever authenticating the user it already is β with
UsePAM no and pubkey-only auth, needing neither /etc/shadow nor setuid.
This is the isolation that matters: user namespaces aren't configured on the workstation, so container root would be
host UID 0 in a runtime escape. Running as the dedicated dev account (UID 1001) means an escape lands as a host
user with no password, no sudo, no files outside /home/dev.
Verify:
ssh <workstation> 'cd ~/devbox && docker compose exec -T devbox ps -o user= -p 1' # dev
ssh <workstation> 'cd ~/devbox && ./bin/devbox logs | grep "Server listening"' # no "must be run as root"Authority is enumerated, not ambient: each credential is scoped, separately revocable, separately attributable.
| Purpose | Credential | Reach |
|---|---|---|
| Agent git (clone, pull, push, commit) | per-repository GitHub App installation token, else a fine-grained PAT | App: one repo, 1h. PAT: its named repos, contents: write |
| Manual git, incl. signing (you) | the laptop's 1Password agent, forwarded per connection (ssh -A devbox) |
same as your laptop; the container stores no private key |
Agent gh on one repository (PRs, issues, runs) |
that repository's GitHub App installation token, else the PAT below | App: one repo, 1h (cached β€30 min on tmpfs) |
Dashboards, CI, issues, account-wide gh |
one fine-grained PAT per GitHub account | named repos, scoped per token |
| LLM inference | per-project GCP service-account key | one dev project, predict-only |
| Project secrets | that project's .env |
one project |
Notably absent: any 1Password account (op isn't installed), any GitHub private key, any Google user credential. See
Secrets.
Can: the whole /home/dev tree β every identity's public keys (useless without the laptop's forwarded agent), every
identity's gh token, each configured App's private key, every project's .env and GCP key β plus the internet, the
container's Tailnet namespace, and the rootless project Docker daemon (Docker).
Cannot: the host filesystem outside the data dir and world-readable paths, the host's root Docker daemon, the devbox container's own lifecycle, root inside the container, any unpublished port, and any 1Password vault.
The consequence: an agent with shell access can push through the same App token or PAT git/gh already resolve, and
read each configured App's private key, every identity's PAT, and every project's .env and GCP key directly. Your own
GitHub push authority stays out of reach β no private key to steal β unless it's inside a ssh -A devbox connection you
forwarded yourself (accepted limit 2).
Important
Treat a compromise as "revoke every identity's PAT and App key, plus the service-account key," not "rebuild a laptop."
These are known and deliberate, not gaps to be closed later:
- No egress filtering. Outbound network is unrestricted β the box needs internet access. An agent reading a poisoned issue or README can send whatever it holds anywhere: IAM and token scoping limit what it can reach, not send. The container is a containment boundary for authority, not confidentiality β assume anything inside can leave.
- A forwarded agent is reachable by anything in that one connection.
ssh -A devboxexposes the 1Password agent socket for the connection's lifetime; a hand-started process inside it β not throughgit, fenced to HTTPS by theomp/claudelaunchers β could callsshdirectly, requesting a signature. 1Password's per-use laptop approval is the backstop: nothing signs without it. Plainherdrpanes,./bin/devbox shell, and a baressh devboxnever forward it. - No isolation between projects. One container, one
devuser, one bind mount: an agent in project A can read project B's.envand GCP key. Cloning something less trusted is where per-project containers or separate users stop being over-engineering. - Secrets are plaintext at rest.
.envfiles,ghtokens, key files and the Claude Code login (~/.claude/.credentials.json) are unencrypted on the workstation's disk, readable by the host user. Workstation disk encryption and host account hygiene are part of this security model, not separate. - The project Docker daemon widens reach to the
devaccount. Anything in the container can start a container through that socket, including one bind-mounting a host path β but only as unprivilegeddev: world-readable host files to read, onlydev-owned files to write, never host root, the root daemon, or your home directory (750, untraversable bydev). The price ofdocker compose upinside the box β why the daemon gets its own dedicated account. - A project port is one firewall rule away from the Tailnet. Rootless Docker binds every published port on
0.0.0.0, with no way to change that βdevbox-docker-firewall, an nftables table dropping input to that daemon's sockets outside loopback and the devbox bridge, confines them../bin/devbox doctorfails if inactive; removed, every project port reaches the Tailnet and LAN. See Docker. - On the laptop, an agent is only as contained as your OS user. The launchers, the
ghshim and the SSH fence choose which credential an agent session uses by default; they cannot stop a process running as you from calling the realghwith your OAuth login, reading the keychain, or asking the 1Password agent for a key (1Password's approval prompt is then the only gate). On the devbox the same bypass gains nothing β no credential broader than the scoped PATs and App keys exists there. Keep autonomous or bypass-permission work against orgs you own on the devbox; on the laptop, agh auth logout(your ownghon fine-grained PATs too) and 1Password set to approve every SSH request remove what a bypass could reach.
An agent's own command allowlist β forbidding op, gcloud and similar β is a useful guardrail, not a control: a
subverted agent can call the same APIs through an SDK without either binary. The boundary is what each credential can
do.
home/.omp/agent/config.yml ships exactly such a guardrail for OMP, seeded when absent by bootstrap and
./bin/devbox agent install: bash.patterns that deny gh auth token|login|β¦, an absolute-path gh (*/bin/gh * β
past the shim), unsetting or reassigning the launcher's exports (env -u, unset GIT_CONFIG_GLOBAL, GH_CONFIG_DIR=β¦;
reading them stays allowed; env -i prompts instead, since this repo's own smoke tests use it), keychain reads,
gh secret/variable/repo delete, --no-verify and force pushes, and prompt for op, gcloud and
terraform apply.
Warning
deny holds even under yolo, but the rules match command text in the bash tool only β not eval, not a script file
β and a project's own bash.patterns replaces the list (settings arrays never merge), so such a repository has to
carry the rules it wants itself.
| Boundary | Rationale |
|---|---|
| No host root Docker socket | Mounting /var/run/docker.sock would hand the sandbox host root, voiding the container's point. Projects needing containers get a sibling rootless daemon owned by a dedicated unprivileged host user, reached through DOCKER_HOST β see Docker for why nesting one needs the container's CAP_SETUID back |
| No root process at runtime | See No root process at runtime |
| No 1Password in the container | A live op session is readable by any agent in that shell, turning a one-project leak into every vault the account can read β while project secrets sit in a plaintext .env regardless, since the app must read them. Secrets render on the laptop, then copy in |
| No Google user credential | gcloud auth application-default login writes a non-expiring refresh token for your whole Google identity; projects get a service-account key scoped to their own GCP project instead |
- The Tailnet is the perimeter. Any Tailnet device with an authorized key can reach the devbox β no second factor on the SSH port.
authorized_keystrusts a GitHub account. Every key on theDEVBOX_GITHUB_USERaccount can log in β the same trust model the workstation's host sshd uses. Narrow it: clearDEVBOX_GITHUB_USER, list keys explicitly inDEVBOX_EXTRA_AUTHORIZED_KEYS.- The
devhost user owns the data, and root can read everything. The bind mount belongs to the dedicateddevaccount, which also owns the project Docker daemon; your host account reaches it only viasudo. The container protects the host from the agent, not the files from its owner.
Overkill for the actual risk (a hostile dependency, not a targeted attacker), and it breaks herdr's attach model. The cheap, high-value controls β non-root, no socket, dropped capabilities, address-scoped port β are already in place.
It would let the container run as root safely, but nothing here needs container root, and userns-remap breaks the
UID-matched bind mount that makes the data dir inspectable from the host β not worth it.
Yes: sshd never changes user. Dropping CHOWN/SETUID/SETGID is exactly what a root-launched sshd would have needed.
Fix the unprivileged path β it's the whole design. cap_add and a root-launched sshd are off the table per
AGENTS.md rule 4; ./bin/devbox shell still gets you in while sshd is broken, so there's no lock-out
risk to trade the boundary away for.
Already the default: agents commit through each identity's own GitHub App where one is installed, scoped to specific
repositories and permissions, and gh uses a fine-grained PAT rather than an account-wide OAuth token. The forwarded
1Password agent stays broad for manual work because it's yours β see accepted limit 2.
Yes β but the primary path is already that tight: an installed GitHub App mints a token scoped to exactly one repository
for one hour. Deploy keys matter only for the fallback case β repos without the App installed β where the fine-grained
PAT trades the same breadth as any multi-repo PAT: it pushes everywhere granted contents: write, readable by any
agent, not just by you at a prompt. Install the App on repos that matter instead of adding a deploy key.
The SSH keys are 1Password items, not files, so the laptop carries no key β but remove the Devbox Laptop item from
GitHub or DEVBOX_EXTRA_AUTHORIZED_KEYS anyway, restart the container (authorized_keys rebuilds on every start), and
drop it from the workstation's own ~/.ssh/authorized_keys. What the laptop does hold in plaintext, if
./bin/devbox agent install ran there, is the agent's own credentials: one GH_TOKEN_<SLUG> per identity in
~/.config/devbox/secrets.env, and each configured identity's App pem under its own app directory. Revoke every
identity's PAT, rotate every App private key β same list as a container compromise above.