Repository navigation
fix(agent-isolation): guard Linux allowAllUnixSockets with denyRead - #1809
Conversation
On Linux, Claude Code ignores sandbox.network.allowUnixSockets and its seccomp filter rejects every AF_UNIX socket, so the container gateway and ssh-agent signing only work with allowAllUnixSockets: true. The setup docs never said so, and nothing warned that the flag lets a sandboxed command connect to every socket it can see: the Docker and Podman daemons, the user D-Bus (systemd-run --user runs commands outside the sandbox), the system bus, X11, and on WSL the interop and WSLg sockets. The reference denyRead of ~/ hides none of them. Document the Linux requirement and the denyRead entries that must go with it, make verify check 12c accept allowAllUnixSockets as the Linux socket half and check 12d require denyRead cover for each host socket path that exists, mirror both in the verification prompt and install Step L, and add a sandbox-lint invariant for the daemon sockets and the per-user runtime directory, with an eval case for 12d. Also catalogue two Linux traps met on the same setup: an allowRead parent of an allowWrite path makes bubblewrap mount it read-only so every Bash call fails with "bwrap: Can't create file ... Read-only file system" (troubleshooting entry, hint-hook pattern, lint check), and denied paths showing up in git status as /dev/null placeholders. Generated-by: Claude Code (Claude Opus 5.5)
The systemd user manager also listens on /run/user/<uid>/systemd/private, so denying only bus and podman under the runtime directory still lets a sandboxed command run systemctl --user; the directory also holds editor askpass and systemd ssh-agent sockets. Require the whole directory of the current uid in sandbox-lint, verify and the docs, and drop the advice to deny single sockets in it. Move the socket-cover requirement into its own verify check 12e, which runs whenever allowAllUnixSockets is set, even with no container runtime (the flag may be there only for ssh-agent signing). Make sandbox-lint list the same paths as the docs and 12e, accept only the current uid, and note it reads only project layers. Correct the ssh-agent caveat (allow the socket or its own subdirectory, never /run/user/<uid>; unverified on Linux), name AF_VSOCK as residual risk, fix the /dev/null placeholder explanation (write-denied paths only), let the bwrap hint match paths with spaces, and add the missing tests. The verify eval case moves to case-23 to avoid a numbering clash. Generated-by: Claude Code (Claude Opus 5.5)
Pasted as-is, "/run/user/<uid>" names a path that does not exist, so the per-user runtime directory, the entry that stops systemd-run --user from escaping the sandbox, stays reachable. Generated-by: Claude Opus 5
b39042d to
2ac2869
Compare
… rebase Generated-by: Claude Opus 5
potiuk
left a comment
There was a problem hiding this comment.
Thanks, this closes a real hole: on Linux the flag silently turns every visible socket into a way out, and the docs, verify 12e and sandbox-lint now say so in the same terms.
I rebased it onto main (conflicts in the troubleshooting table, test_validator.py, and the generated stamps/fingerprint) and added one small fixup: the jsonc example now says to replace <uid> with id -u. Pasted literally, /run/user/<uid> would deny nothing, and it's the entry that stops systemd-run --user.
One non-blocking question for a follow-up: the example names ~/.claude/settings.local.json as a per-machine file. I'm not sure Claude Code reads a user-scope settings.local.json (the documented local file is the project's .claude/settings.local.json). If it doesn't, the Linux half would silently not apply from there.
Drafted-by: Claude Code (Opus 5.5); reviewed by @potiuk before posting
* docs(spec-loop): sync specs with contributed functionality Back-fill the specs for the 46 commits since the last sync that changed behaviour without touching a spec: the public-surface scrub mod (#1676), adapter hardening (#1862), the hook managers with install strategy `none` (#1838), Linux `allowAllUnixSockets` and its socket cover (#1809), the touch-overlay dismissal and matcher (#1832, #1833), the per-family Codex manifests (#1836), Vulnogram reviewer comments (#1840), the verify false-pass fixes (#1808), gateway refusal hints (#1810), adversarial-review output and agy fixes (#1852, #1854, #1871), and code-review inline comment text (#1853). Also correct drift found on the way: the threat-model pointer now names Appendix B after the #1859 restructure, the troubleshooting catalogue has fourteen entries, and the overview's Mentoring and Pairing skill counts match docs/modes.md. Generated-by: Claude Opus 5 * docs(marketplace): magpie-security ships its own Codex manifest too #1676 gave magpie-security a .codex-plugin/plugin.json and an empty hooks/codex-hooks.json, because its hooks.json now carries a Claude Code mod, but the "Which manifest Codex reads" table and the Hooks paragraph still named only magpie-setup and magpie-utilities. Generated-by: Claude Opus 5
Summary
allowAllUnixSocketsis undocumented and unguarded.AF_UNIX, unlesssandbox.network.allowAllUnixSocketsistrue. The container gateway and ssh-agent need it, becauseallowUnixSocketsonly applies on macOS./run/docker.sock, Docker Desktop's/run/docker-cli.sock, all of/run/user/<uid>(D-Bus andsystemd/private, i.e.systemd-run --user),/run/dbus, X11, and on WSL/run/WSLinterop and/mnt/wslg.sandbox.filesystem.denyReadentries for those paths, and namesAF_VSOCKas the residual riskdenyReadcannot hide. It also makes verify check 12c Linux-consistent and adds a check 12e that requires the socketdenyReadcoverage whenever the flag is on, even when check 12 is otherwise n/a. Install Step L.3 andtools/sandbox-lintget matching rules and tests.allowReadmakes the project read-only. AnallowReadentry that is a strict parent of anallowWritepath makes bubblewrap mount the write path read-only. Every Bash call then fails withbwrap: Can't create file … Read-only file system. This PR adds a troubleshooting entry, asandbox-error-hint.shpattern, and a sandbox-lint rule.git status. A troubleshooting entry explains why write-protected paths that do not exist appear as character devices1,3and are listed as untracked.Type of change
.claude/skills/<name>/) — eval fixtures updated belowtools/*/withpyproject.toml)docs/,README.md,CONTRIBUTING.md)Test plan
prek run --all-files --skip workspace-pytestpassesprek run workspace-pytest --from-ref upstream/main --to-ref HEADpasses;tools/sandbox-lintpytest and the agent-isolationtest_sandbox_error_hint.pypass. New tests cover:os.getuid(), plus~and relative entries;~, a trailing slash, and.;/tmphint.skill-eval --cli "claude -p" tools/skill-evals/evals/setup-isolated-setup-verify/: 23/28 pass.upstream/main(step-1 case-7; step-2 case-3, case-5, case-6).case-23-linux-all-unix-sockets-exposedpassed 3 of 4 runs.denyReadentries added, the host sockets are no longer sockets inside the sandbox (test -Sis false)./tmpkeep working:docker versionthrough the gateway andssh-keygen -Y signboth succeed.allowReadfailure was reproduced and fixed by listing the specific directories.denyReadpaths, so listing paths that a host lacks is harmless.RFC-AI-0004 compliance
check-placeholderspasses)Notes for reviewers (optional)
allowReadinside a denied/run/user/<uid>re-opens an agent socket that lives there. The doc marks it as unverified.allowReadtrap is Claude Code's mount ordering, so it may also be worth an upstream Claude Code report. The lint rule fails closed, including outsidedenyRead.Generative AI: drafted with Claude Code (Claude Opus 5.5) and checked by an independent Claude Opus review agent; opened as a draft for the author's line-by-line review before it is marked ready.
🤖 Generated with Claude Code