Repository navigation
Protect host-trusted files from the sandbox - #4
Merged
Merged
Conversation
claude on the host runs the hooks and statusline command in the
profile's settings.json, and the VM sees the profile dir read-write. The
VM got a copy mounted over the file only when there was a sudo deny to
strip; otherwise it wrote the real file. It now always gets a per-launch
copy.
A missing settings.json is created as {} so there is a file to mount
over, and a symlinked one is never read through: the VM could have aimed
it at another profile's credentials, so the VM gets {} and a warning
instead. Failing to write the copy now stops the launch.
The work tree and its git dir are mounted read-write, so the VM could set core.fsmonitor or an alias in .git/config, or plant a hook, and the host's next git command in the repo would run it, a shell prompt's git status included. Each git dir's config, the repo's and its submodules', is now mounted read-only over itself, and hooks/ is replaced by a throwaway copy refreshed from the host at each launch, with links copied as links. krun refuses a rename onto a file mount, so git commands that save config (push -u, branch -u, remote add) cannot save it in the VM. The push itself still works; the sandbox briefing tells the agent.
claude loads hooks from .claude/settings.local.json at the work tree root and at the launch dir, and git ignores the file, so a change made in the VM would run on the host unnoticed. Where the file exists, the VM now gets a per-launch copy, never read through a link.
The VM gets the global statusline script read-only but can replace the profile's link to it, and claude runs the statusline on every host launch. A host launch of a profile that is marked for the sandbox, or has been launched in one, now warns unless statusline.sh is absent or is the link to ~/.claude/statusline.sh that add creates.
The profile's .claude.json stays writable from the VM, and claude on the host starts the MCP servers it lists. Supervised launches now record the MCP servers, the .mcp.json approvals and which .claude/settings.local.json files exist before the VM starts, and list what the session added or changed once it ends. Only names are printed, never commands or env, which carry tokens. The names come from the VM, so non-printable characters are replaced and rich markup is escaped before printing, as is the link target that _linked_mounts prints.
The security section said the agent could change git config, hooks, settings.json and .claude/ settings that the host later runs. It now lists what is protected (read-only git config, throwaway copies of the hooks and settings files, the statusline check, the post-session MCP report) and what still needs reviewing.
The override decides every launch, but `sandbox <name>` and `list` only read the .sandbox marker, so with it set they could report a microVM for a profile that launches on the host. Both now say when the override is set and what it does, and `sandbox --on/--off` says so when the override contradicts the new setting. They also read the marker the way the launch does: a dangling marker link counts as sandboxed. `sandbox --on` replaces a linked marker instead of touching, and so creating, the file the link points at.
The sandbox's copy of the profile's settings.json drops deny rules such as Bash(sudo *), which would otherwise block the VM's own scoped sudo (deny wins even under --dangerously-skip-permissions). Shared settings from shared-settings.json reach claude through --settings and kept them, so a shared sudo deny still blocked sudo dnf and sudo podman in the VM. Sandbox launches now drop the same denies from the shared settings; host launches keep them.
Both were mounted straight from the host with :z, which relabels the user's own files for containers (known_hosts loses its ssh_home_t label) out from under the host's tools and restorecon. The VM now gets copies in the profile's state dir, refreshed every launch, and :z relabels those, as the launcher already does for the claude binary.
When the host has custom CA anchors, the entrypoint ran update-ca-trust extract on every boot, which takes about 7 s in the VM. The host's own update-ca-trust has already merged those anchors with its system roots into /etc/pki/ca-trust/extracted, so the launcher now mounts that directory read-only at the same path, alongside the anchors, and the entrypoint no longer extracts. Boot to exit fell from 13.7 s to 4.6 s on a host with six anchors, with every anchor still trusted. Anchors without an extracted directory warn and mount neither.
Each host update of claude copied the 244 MiB native binary into the data dir for the sandbox. On Python 3.13, shutil.copy writes every block of it, and snapshots then keep each old version pinned. The copy now asks the kernel to share the binary's blocks (FICLONE), which is instant and takes no space on btrfs or XFS, and falls back to a plain copy elsewhere or across filesystems.
podman run passes the host's http_proxy, https_proxy, no_proxy and the like into the container by default, so a proxy URL's credentials reached the VM although the launcher forwards nothing else from the host environment. Sandbox launches now pass --http-proxy=false; a proxy the VM needs can be forwarded with CLAUDE_PROFILE_SANDBOX_FORWARD_ENV.
The digest-pinned FROM line and the printf that writes storage.conf ran past 100 characters. The digest moves into an ARG ahead of FROM, and printf takes one argument per line as the sudoers step does. The build resolves the same base digest and writes a byte-identical storage.conf.
The security section described what the VM can change but not which credentials it can read or use. It now lists what every launch hands the VM (the profile's Claude login, .env values, MCP tokens in .claude.json and ancestor .mcp.json files, a copy of ~/.gitconfig), what each opt-in setting adds, and what is not mounted.
5 of 6 tasks
Three tests looked for a word that the warning only prints inside a long temp path. rich wraps such a path mid-word at its 80-column width, so the word was split whenever the temp dir's length put a line break inside it: CI's /tmp/pytest-of-runner paths did, locally they missed. The assertions now ignore whitespace, as the other path checks do, and pass at each of 41 temp-dir lengths.
Sandbox fixes: launch status, shared denies, CA trust, copies
3 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3: merge that first. Once its branch is deleted, GitHub retargets this pull request to master.
Keeps the sandboxed agent from changing files the host later runs, or reports when it does.
What it does
config, the repo's and every submodule's, is mounted read-only over itself, so the VM can't setcore.fsmonitor,core.sshCommand, an alias or the like that the host's next git command would run (a shell prompt'sgit statusincluded, even while the VM runs).hooks/becomes a throwaway copy refreshed from the host at each launch, with links copied as links: the host's hooks still run in the VM andprek installworks there. krun refuses a rename onto a file mount, so git commands that save config can't save it:git push -ustill pushes but can't record the upstream,branch -ucan't set it (both print an error and exit 0), andgit remote addfails. The sandbox briefing tells the agent.settings.jsonis now always a per-launch copy in the VM, not only when a sudo deny needs stripping. A missing one is created as{}so there is a file to mount over, and a symlinked one is never read through. Existing.claude/settings.local.jsonfiles at the work-tree root and the launch dir get per-launch copies too. claude falls back to an in-place write when its rename fails, so settings changes still work in the VM.statusline.shis not the link to~/.claude/statusline.sh..mcp.jsonapprovals and new.claude/settings.local.jsonfiles a session added or changed. Names only, never commands or env; names from the VM have non-printable characters replaced and rich markup escaped, as does the link target_linked_mountsprints.Test plan
prek run --all-filespasses (378 tests)