Mount ~/specs read-write; add bubblewrap/socat (partial fix)

~/.claude/CLAUDE.md (already carried into the container) tells agents to
write specs to ~/specs/ instead of a project-local path - that instruction
was already in effect inside the sandbox, but nothing mounted the
directory it points at, so an agent following it would write into a path
that vanishes with the container. Bind-mounted read-write at the fixed
host path ~/specs (auto-created if missing), same pattern as the shared
scratch space but not configurable - it's tied to the CLAUDE.md
convention, not a per-run choice.

Also adds bubblewrap/socat, which Claude Code checks for before offering
its own internal per-command sandboxing and /remote-control. This clears
the "Sandbox disabled: dependencies are missing" warning, but doesn't
actually fix /remote-control: bwrap still can't create a namespace on this
host ("kernel does not allow non-privileged user namespaces"), and even
adding CAP_SYS_ADMIN to the container only got further to a mount-
propagation permission error, not further progress. Making it actually
work would need privilege escalation approaching full --privileged mode,
which would undo most of this session's hardening - not pursued. Kept the
packages anyway since they at least make the startup warning accurate
rather than noise, in case a future decision revisits this tradeoff.

All 22 existing tests still pass.
This commit is contained in:
2026-07-19 23:45:01 -07:00
parent 832aaef3d8
commit 0b7857eb3f
4 changed files with 29 additions and 2 deletions
+6
View File
@@ -58,6 +58,12 @@ fi
SANDBOX_SHARE_DIR="${SANDBOX_SHARE_DIR:-$HOME/.local/share/claude-sandbox/shared}"
mkdir -p "$SANDBOX_SHARE_DIR"
# ~/.claude/CLAUDE.md (carried into the container) tells agents to write
# specs to ~/specs/ rather than a project-local path - make sure that
# actually exists before it's bind-mounted, for a fresh machine that hasn't
# used the convention yet.
mkdir -p "$HOME/specs"
export WORKSPACE_DIR
export SANDBOX_SHARE_DIR
export SSH_AUTH_SOCK
+9 -2
View File
@@ -8,8 +8,13 @@ ENV TZ="$TZ"
ARG CLAUDE_CODE_VERSION=latest
# Install basic development tools, iptables/ipset (for the egress firewall),
# openssh-client (for git-over-ssh and agent forwarding), and the toolchain
# nvim's plugin config (config/nvim, telescope/treesitter/mason) expects.
# openssh-client (for git-over-ssh and agent forwarding), the toolchain
# nvim's plugin config (config/nvim, telescope/treesitter/mason) expects, and
# bubblewrap/socat, which Claude Code's own internal per-command sandboxing
# (~/.claude/settings.json's "sandbox" block - separate from and in addition
# to this container) and /remote-control both need to even be offered,
# without which Claude Code detects the environment can't support them and
# silently disables both rather than erroring when used.
RUN apt-get update && apt-get install -y --no-install-recommends \
less \
git \
@@ -42,6 +47,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
cowsay \
fortune-mod \
fortunes-min \
bubblewrap \
socat \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& ln -s "$(command -v fdfind)" /usr/local/bin/fd
+7
View File
@@ -83,6 +83,13 @@ touching the actual project checkout. It persists across sessions since it's
a real directory, not a container volume. Set `SANDBOX_SHARE_DIR` to point
it somewhere else.
`~/specs` is also bind-mounted read-write, at the fixed host path `~/specs`
(created automatically if it doesn't exist yet) - not configurable like
`SANDBOX_SHARE_DIR`, since it exists to make the global `CLAUDE.md`
convention of writing specs to `~/specs/` (also carried into the container)
actually work there instead of writing into a path that vanishes with the
session.
## Sandbox self-awareness
A `SessionStart` hook (`~/.claude/hooks/sandbox-context.sh`, registered in
+7
View File
@@ -61,6 +61,13 @@ services:
# since most of its content must stay writable for normal operation.
# See README Security notes for this residual risk.
- ${HOME}/.claude.json:/home/node/.claude.json
# ~/.claude/CLAUDE.md (mounted above) tells agents to write specs to
# ~/specs/ rather than a project-local path - that instruction is
# already in effect inside the container (CLAUDE.md carries over), so
# the directory it points at needs to actually exist and be writable
# here too, or an agent following it just writes into an ephemeral
# path inside the container that vanishes with the session.
- ${HOME}/specs:/home/node/specs
# ~/.claude/hooks is a symlink into ~/dotfiles/config/claude/hooks (an
# absolute host path); mount dotfiles at the same path so it resolves
# in the container too. Read-only: hooks are only ever read/executed