Repository navigation
Conversation
…pository (#448) Agents branch in a worktree of their own (`git worktree add <path> -b <branch>`), but inside the sandbox git couldn't make the admin dir under the read-only `<common>/worktrees/`, and the checkout path was outside the workspace. - `<common>/worktrees/` is writable, bound at its own path (it can't be renamed away and rebuilt). - The repository's worktree locations are writable: the sibling `<repo>-worktrees/` (codeoid's own convention), `<repo>/.worktrees/`, and `sandbox.worktreeRoots` (`CODEOID_SANDBOX_WORKTREE_ROOTS`, Settings > Sandbox). Never `/`, the home, one holding the repository or inside its git dir or a hidden path, one another user owns, or one that is (or is reached through) a link a command could have planted. - Every worktree that isn't the session's own stays held: its admin dir whole (not rewritten, renamed, removed or pruned), its `.git` file, and its whole checkout when that is in a location (`remove --force` deletes nothing of it). The session's own: appeared since its sandbox first looked, checked out where its commands can write, not made by codeoid (forks and loops register theirs). - Codex gets the locations as writable roots; the end-of-turn warning reads a worktree as git makes it as nothing changed. Tested with real bubblewrap (add, commit, rebase, list, remove, prune; each protection) and the real Claude CLI's sandbox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntainers (#448) The sandbox hides container runtime sockets on purpose (root on the machine), so testcontainers suites couldn't run. Docker access lets the owner opt a session in; it is off by default. - `sandbox.docker.socket` (`CODEOID_SANDBOX_DOCKER_SOCKET`, Settings > Sandbox) daemon-wide; per session `dockerSocket` in the sandbox setting: `codeoid new … --docker-socket`, the new-session dialog's box, `/sandbox <provider> … docker` (`nodocker`), `codeoid sandbox set … --[no-]docker-socket`, routines and loops `--docker-socket`. - Commands reach the host's runtime sockets that exist (`$DOCKER_HOST`'s, Docker's, rootless Docker's, Podman's; Docker Desktop's on macOS), each at its own path, read-only, with `DOCKER_HOST` set; never the client's credentials (`~/.docker` stays hidden). bwrap binds them, Seatbelt allows them last, the Docker provider mounts the runtime's socket at /var/run/docker.sock with its group, Claude's own sandbox allows them (`allowAllUnixSockets` on Linux, where its seccomp refuses unix sockets otherwise). The CLI's git shim never gets them. - Turning it on is a widening (`widens`): the owner-only confirm on the session, session:approve and an undelegated token at create, fork, routine and loop; daemon-wide it needs settings:write, undelegated. Autonomous on such a session is the owner's own call. - Said plainly: the chip "sandboxed, Docker access" (warn, "Docker access can reach the whole machine"), `codeoid ls`, and every approval card. Tested with a planted listening socket under real bubblewrap and the real Claude CLI's sandbox (no Docker daemon is reachable here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in their own folder, worktreeRoots are validated and scoped per repository, and the end-of-turn warning covers the agent's worktrees The worktree locations were writable whole, and the session's spec was built once (Claude native: at its first query, for the session's life), so a session could write a fork's, a loop's or another session's checkout made later (its .claude settings, its .git file, its admin dir), and a shared worktreeRoots dir exposed other repositories' worktrees. - Each location (`<repo>-worktrees/`, `<repo>/.worktrees/`, `<root>/<repo>/` for a worktree root) holds one folder per session (`sessionFolderName`: its name and short id, found again by the id after a rename or a restart); only that folder is writable, the rest of a location read-only (later arrivals included), a location inside the workspace held whole. The agent is told where in its system prompt (every backend). - Every admin dir not checked out in the session's folder is held; the session's spec is built again when an admin dir appears or goes (a Claude query is rebuilt at its next turn). - sandbox.worktreeRoots: refused at config load, settings write and env (`/`, the home or a dir holding it, a hidden dir of the home, ~/Library, codeoid's config dir, a system dir, a relative root climbing out); a missing root is made only beside the repository, else said on the limits line. - The end-of-turn look names a checkout in the session's folder whose .git names anything but its own admin dir (or a planted .git dir), and an admin dir that changed; a new plain one or a removed one isn't news. - On Claude's own sandbox a location isn't shown read-only (its CLI lays readable binds over writable ones, which would make the folder read-only). Tested with real bubblewrap (own folder add/commit/remove, elsewhere refused, other sessions', the user's and codeoid's checkouts read-only, later arrivals, admin dirs, shared roots, the warning) and the real Claude CLI's sandbox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kets, the system bus and (with Docker access on Claude's Linux sandbox) the host's /tmp are out of a sandboxed command's reach - Container and VM runtime sockets hidden in every sandbox, Docker access showing only the runtime's own: libvirt (`/run/libvirt`, `$XDG_RUNTIME_DIR/libvirt`), LXD, Incus, dockerd's `/run/docker`, cri-dockerd, k0s, microk8s, firecracker-containerd, balena, besides those already listed. The system D-Bus and screen's dir are hidden too. - Claude's Linux sandbox can only allow every unix socket (`allowAllUnixSockets`), so with Docker access the host's /tmp and /var/tmp are denied there (tmux, X11, ssh-agent, IDE sockets); what stays reachable is said on the limits line and in the owner's confirm. - A runtime socket the daemon's user can't connect to isn't counted: Docker access then says so and opens nothing up. Checked on the real Claude CLI (a tmux-style socket planted in the host's /tmp is out of reach, the runtime's is reached, its network proxy still works) and real bubblewrap (a planted libvirt socket stays hidden with Docker access on; the host's own libvirt socket is unreachable). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… services' sockets (Postgres, MySQL, Redis) are out of a sandboxed command's reach A filesystem socket isn't the network: --unshare-net doesn't stop a command connecting to /run/postgresql's, which lets it in by who the user is (peer auth). policy.ts `localServiceSocketPaths` (the system's database socket dirs, and those found by name in /tmp, /private/tmp, /var/tmp) become SandboxSpec.serviceSockets when the network the session asked for isn't on; bubblewrap, Seatbelt and Docker hide them, Claude's own sandbox denies them (the /tmp ones are behind its socket block or the denied /tmp), and the host file tools' bounds count them hidden. With the network on, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… <repo>/.worktrees is no longer a worktree location Every sandboxed session in a repository used to make an empty `.worktrees/` in the user's checkout. The sibling `<repo>-worktrees/` (and configured roots) are now the only locations; a `.worktrees/` the user already has is held read-only whole for a session working in that checkout, and gives no session a folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…udged where it leads, so ../.config of a repository in the home is refused worktreeRootProblem ran only on absolute and `~` roots; a relative one was resolved against the main checkout unchecked (`../.config` of `~/proj` gave a writable `~/.config/proj/<folder>`). worktreeLocationDirs now judges it again once resolved (links and all) by the same list, refuses one inside the main checkout, and compares without regard to case on macOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lds a session's sandbox, and a worktree's admin dir changed inside it is restored at the turn's end and said The spec was rebuilt whenever admin dir names changed, which rebuilt a running Claude query and killed its background tasks. Yet an admin dir made while a sandbox ran could still be taken over for good and unseen: its gitdir pointed into the session's folder (the next spec then took it for the session's own), or the dir replaced by a link to a planted git dir. The spec is now built once (and at a natural rebuild). worktree-entries.ts `WorktreeEntryGuard` checks every admin dir against git's layout when the spec is built, at every turn's end and at destroy: - not a link (one is removed: git there stops instead of following it); - commondir is the repository's git dir; - no config, hooks/ or info/ (but sparse-checkout); - config.worktree empty or the user's; - gitdir names a checkout that names it back, the one last seen. It restores what is certain: the gitdir from the back-link found by scanning, never from the changed file. What it takes out is moved aside, never deleted, and so is a duplicate a `git worktree repair` would adopt (one already adopted is undone). What can't be restored with certainty is said and left. The session shows a warning line (sandbox.worktree_entry) and audits session.worktree_entry_restored / _unrestored. An entry is the session's own only when it made it (first seen in its folder, claimed by no other checkout), recorded per inode, so a reused name is judged afresh. Empty worktree folders, and an emptied <repo>-worktrees/, are removed at a turn's end with nothing running, and made again at the next turn's start. plainWorktree treats a linked admin dir as custom. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ket, and the socket tests count only what they planted DOCKER_HOST named the root daemon's /var/run/docker.sock when the user's rootless one existed too; the user's runtime comes first now (as Docker Desktop's already does on macOS). The tests asserted no other runtime socket exists, which fails on any machine with Docker, CI's runners included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rsharath
approved these changes
Nov 7, 2026
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.
Summary
Two everyday workflows that the OS sandbox (#454) broke now work:
Both keep the sandbox's guarantees, and the limits that remain are stated plainly.
Why
A real session hit both:
git worktree add, which failed with "Operation not permitted" under.git/worktrees/.Worktrees: one folder per session
<repo>-worktrees/<session-folder>/…, plus anysandbox.worktreeRootsentries, scoped per repository. Codeoid tells the agent the path when the session starts.git worktree add, commit, rebase,removeandpruneall work, in every mode.--worktreecheckouts, and any folder created later by anyone else..git/worktrees/) stays protected:session.worktree_entry_restoredin the audit log).sandbox.worktreeRootsis validated after resolving. It refuses your home,/, and any dir whose contents run outside the sandbox (~/.config,~/.local, systemd units, LaunchAgents, codeoid's dirs).Docker access: opt-in
sandbox.docker.socket, off by default. It's in Settings → Sandbox,CODEOID_SANDBOX_DOCKER_SOCKET, per session/sandbox auto docker,codeoid new … --docker-socket, and the new-session dialog.DOCKER_HOSTset to that socket. The user's own (rootless) runtime is preferred over the root daemon's.~/.docker/config.json)./tmpand other shared socket dirs are hidden, and the runtime's socket is the only one reachable.Also tightened
Verification
main.docs/sandbox.md.🤖 Implementation context (for agents / maintainers)
src/daemon/sandbox/policy.ts: worktree locations and the per-session folder;worktreeRootsvalidation and per-repo scoping;dockerSocketPaths(order:DOCKER_HOST, the user's runtime, then system); hidden runtime and service sockets (HOST_SOCKET_DIRS); git layout pinning.session.worktree_entry_restored. See the round-11 fix commit.bwrap.ts,seatbelt.ts,docker.ts,claude-native.ts(allowAllUnixSocketswith Docker access, plus the narrowing).SessionSandboxInfo.dockerSocketand limits lines, coresandboxBadge, webSandboxChip/NewSessionModal/ settings, CLI--docker-socket,/sandbox … docker.docs/sandbox.md("Git worktrees inside the sandbox", "Docker access"), CHANGELOG, CLAUDE.md.🤖 Generated with Claude Code