Two related fixes to docker/claude-sandbox/, from the same session:
Container user now mirrors the real host user (username/uid/gid/home),
via new Dockerfile build args (defaulted to today's exact node/1000/1000/
/home/node, so nothing changes unless bin/claude-sandbox actually overrides
them). This fixes a real bug: Claude Code plugin/skill resolution was
silently broken inside the sandbox because ~/.claude/plugins/
installed_plugins.json records install paths as literal host-absolute
strings (e.g. /home/brad/.claude/plugins/...), which never existed inside
a container whose $HOME was hardcoded to /home/node - confirmed live
before and after the fix. It also lets the existing dotfiles dual-mount
(a workaround for the same underlying $HOME mismatch) collapse to one
line, and requires tagging the built image per-user
(claude-sandbox:) so two accounts on a shared devbox don't
clobber each other's build.
Dedicated in-container SSH agent for headless git push. Motivation:
running the sandbox on a devbox over SSH+tmux means the forwarded agent
belongs to whichever client connected - when that client disconnects, the
forwarded socket dies, breaking git push for anything still running
headless. An optional SANDBOX_SSH_KEY now points at a purpose-built,
passphrase-less keypair (a Gitea deploy key, not your personal identity)
that entrypoint.sh loads into a fresh ssh-agent running inside the
container itself, independent of the forwarded socket - the plaintext key
is deleted immediately after being loaded into the agent, so nothing
readable is left on disk. Fully opt-in; forwarding-only behavior is
unchanged if SANDBOX_SSH_KEY isn't set.
Testing
Full bats suite (docker/claude-sandbox/test/{static,runtime}.bats): 40/40
passing, including 7 new tests covering both changes (host-identity
build-arg override end-to-end, dedicated-key agent load with no forwarded
socket at all, plaintext-key deletion regression guard, optional-mount
patterns, etc.)
Verified live: a container built with overridden SANDBOX_USER/UID/HOME
actually comes up as that user; the plugin/skill path that was previously
unreachable now resolves; a throwaway ed25519 key loads into the
in-container agent and its plaintext copy is gone afterward;
bin/claude-sandbox's validation gate correctly fails fast with neither a
forwarded agent nor a dedicated key, and passes through when only the
dedicated key is set.
Confirmed no regressions: every pre-existing static/runtime test still
passes unchanged against the new defaults.
**Summary generated by LLM agent**
## Summary
Two related fixes to docker/claude-sandbox/, from the same session:
1. **Container user now mirrors the real host user** (username/uid/gid/home),
via new Dockerfile build args (defaulted to today's exact node/1000/1000/
/home/node, so nothing changes unless bin/claude-sandbox actually overrides
them). This fixes a real bug: Claude Code plugin/skill resolution was
silently broken inside the sandbox because ~/.claude/plugins/
installed_plugins.json records install paths as literal host-absolute
strings (e.g. /home/brad/.claude/plugins/...), which never existed inside
a container whose $HOME was hardcoded to /home/node - confirmed live
before and after the fix. It also lets the existing dotfiles dual-mount
(a workaround for the same underlying $HOME mismatch) collapse to one
line, and requires tagging the built image per-user
(claude-sandbox:<username>) so two accounts on a shared devbox don't
clobber each other's build.
2. **Dedicated in-container SSH agent for headless git push.** Motivation:
running the sandbox on a devbox over SSH+tmux means the forwarded agent
belongs to whichever client connected - when that client disconnects, the
forwarded socket dies, breaking git push for anything still running
headless. An optional SANDBOX_SSH_KEY now points at a purpose-built,
passphrase-less keypair (a Gitea deploy key, not your personal identity)
that entrypoint.sh loads into a fresh ssh-agent running inside the
container itself, independent of the forwarded socket - the plaintext key
is deleted immediately after being loaded into the agent, so nothing
readable is left on disk. Fully opt-in; forwarding-only behavior is
unchanged if SANDBOX_SSH_KEY isn't set.
## Testing
- Full bats suite (docker/claude-sandbox/test/{static,runtime}.bats): 40/40
passing, including 7 new tests covering both changes (host-identity
build-arg override end-to-end, dedicated-key agent load with no forwarded
socket at all, plaintext-key deletion regression guard, optional-mount
patterns, etc.)
- Verified live: a container built with overridden SANDBOX_USER/UID/HOME
actually comes up as that user; the plugin/skill path that was previously
unreachable now resolves; a throwaway ed25519 key loads into the
in-container agent and its plaintext copy is gone afterward;
bin/claude-sandbox's validation gate correctly fails fast with neither a
forwarded agent nor a dedicated key, and passes through when only the
dedicated key is set.
- Confirmed no regressions: every pre-existing static/runtime test still
passes unchanged against the new defaults.
Design doc: ~/specs/2026-07-21-claude-sandbox-ssh-and-plugin-mounts-design.md
Implementation plan: ~/specs/2026-07-21-claude-sandbox-ssh-and-plugin-mounts-plan.md
The headless-SSH-key feature claimed the plaintext key was "deleted
immediately after being loaded into the agent, so nothing readable is
left on disk" - but entrypoint.sh only deleted the copy it made; the
original bind-mounted source at $SANDBOX_HOME/.ssh-sandbox-key-src stayed
mounted, readable, for the container's whole life. Combined with the
sandbox user's uid now matching the host user's, that file was directly
readable by the very process the sandbox exists to constrain, letting a
compromised session exfiltrate the raw key instead of being limited to
signing through the local agent socket.
Move the mount to /root/.ssh-sandbox-key-src (root:root, 700 by default)
so only entrypoint.sh's root-run startup code can ever reach it; the
sandbox user has no path to that directory at all. Update the matching
static/runtime tests (the runtime one now asserts the sandbox user
actually gets permission denied reading the source) and correct the
README's description of the mechanism.
Second-round review follow-up. Security notes was written as the
canonical list of residual risks but never mentioned the new
SANDBOX_SSH_KEY mechanism, which has the same "signs for any repo it can
reach" shape as the forwarded agent (already documented) - add a bullet
covering it and the /root-only mount.
Also nothing enforced that SANDBOX_SSH_KEY actually be a dedicated
low-privilege key rather than a real personal identity, which is the
whole basis for accepting it as passphrase-less and left loaded for an
entire unattended session. bin/claude-sandbox now warns (non-fatal) if
it looks like a default personal keyfile (id_rsa, id_ed25519, etc).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
Summary
Two related fixes to docker/claude-sandbox/, from the same session:
Container user now mirrors the real host user (username/uid/gid/home),
via new Dockerfile build args (defaulted to today's exact node/1000/1000/
/home/node, so nothing changes unless bin/claude-sandbox actually overrides
them). This fixes a real bug: Claude Code plugin/skill resolution was
silently broken inside the sandbox because ~/.claude/plugins/
installed_plugins.json records install paths as literal host-absolute
strings (e.g. /home/brad/.claude/plugins/...), which never existed inside
a container whose $HOME was hardcoded to /home/node - confirmed live
before and after the fix. It also lets the existing dotfiles dual-mount
(a workaround for the same underlying $HOME mismatch) collapse to one
line, and requires tagging the built image per-user
(claude-sandbox:) so two accounts on a shared devbox don't
clobber each other's build.
Dedicated in-container SSH agent for headless git push. Motivation:
running the sandbox on a devbox over SSH+tmux means the forwarded agent
belongs to whichever client connected - when that client disconnects, the
forwarded socket dies, breaking git push for anything still running
headless. An optional SANDBOX_SSH_KEY now points at a purpose-built,
passphrase-less keypair (a Gitea deploy key, not your personal identity)
that entrypoint.sh loads into a fresh ssh-agent running inside the
container itself, independent of the forwarded socket - the plaintext key
is deleted immediately after being loaded into the agent, so nothing
readable is left on disk. Fully opt-in; forwarding-only behavior is
unchanged if SANDBOX_SSH_KEY isn't set.
Testing
passing, including 7 new tests covering both changes (host-identity
build-arg override end-to-end, dedicated-key agent load with no forwarded
socket at all, plaintext-key deletion regression guard, optional-mount
patterns, etc.)
actually comes up as that user; the plugin/skill path that was previously
unreachable now resolves; a throwaway ed25519 key loads into the
in-container agent and its plaintext copy is gone afterward;
bin/claude-sandbox's validation gate correctly fails fast with neither a
forwarded agent nor a dedicated key, and passes through when only the
dedicated key is set.
passes unchanged against the new defaults.
Design doc: ~/specs/2026-07-21-claude-sandbox-ssh-and-plugin-mounts-design.md
Implementation plan: ~/specs/2026-07-21-claude-sandbox-ssh-and-plugin-mounts-plan.md