Skip to content

Bundled Fragments

The default policy root ships base.sbx plus a fragment tree. Subdirectories are organisational only — the resolver does not read meaning into them.

Always loaded, always first, never overridden. It sets the floor:

  • grants the minimal read surface a process needs to start — /usr, /bin, /lib, /nix/store — plus read and write on $WORKDIR and $TMPDIR
  • denies a short, non-negotiable list outright: ~/.ssh, shell history and rc files, the wardcell binary
  • grants no network at all, so nothing resolves or connects

Everything else in your home directory is unreachable by omission rather than by a deny — which is what lets an opt-in fragment grant ~/.aws without tripping the base-override check. Base’s explicit denies cannot be loosened by any layer.

One per coding agent: claude-code, codex and pi. Each matches on the agent’s command name and grants its config directory, its own sockets and the hosts a drafted run reached — and each requires shared/agent-common, which carries what all three needed, git included.

Terminal window
wardcell run -- claude # picks up agents/claude-code.sbx

codex also adds one switch, reported like any other: its own sandbox wraps each tool call in a second Seatbelt profile, macOS allows one per process, and wardcell has used it — so without --sandbox=danger-full-access every tool call fails and the CLI says so only in prose, while exiting 0. Wardcell is the sandbox in that arrangement; the CLI’s approval prompts are left alone, which is why the narrower switch is used rather than the one that also bypasses them. Set the option yourself, in either spelling, and yours is the one that applies.

All three were drafted by running the real CLI under wardcell policy draft, so what is granted is what a run was denied. What each one refuses is in its comments, and the refusals are as deliberate as the grants: telemetry endpoints, api.github.com (the developer’s token reaches every repository it can), the shell startup files (where exported API keys live), and a listing of the home directory itself.

macOS app bundles, selected by bundle id via selects_on { app_bundles }. The bundle identifier is not something you type — wardcell reads it out of the bundle’s Info.plist, along with the executable to launch:

Terminal window
wardcell run -- /Applications/Claude.app # picks up apps/claude-app.sbx
wardcell run -- /Applications/Codex.app
wardcell run -- "/Applications/Visual Studio Code.app"

claude-app, codex-app and vscode-app, each granting its own bundle (via the $APP_BUNDLE token), its support and cache directories, and its hosts. All three requires shared/electron-app, which carries what Chromium needs in order to start at all.

No flags needed: shared/electron-app appends --no-sandbox through an args block, because Chromium’s own sandbox cannot start inside wardcell’s, and each run reports what it added. codex-app and vscode-app additionally pin this run’s CA, so their hosts are granted with allow_http and every request is gated; claude-app uses allow_tcp, because that application refuses to start when the pinning switch is present. See macOS: Chromium applications.

vscode-app also runs the editor against its own directories under ~/.vscode-wardcell: user data per zone and workspace (<zone>/$WARDCELL_WORKDIR_ID), extensions per zone (<zone>/extensions), and its cross-profile shared storage per zone (<zone>/shared). The first is what makes the run a run — the editor is single-instance, so without it an editor already open outside wardcell would be handed the arguments while the sandboxed process exited, a run that sandboxed nothing and said so nowhere, and an instance belonging to another zone would be reachable in the same way. The second keeps an extension installed inside one sandbox from being loaded by the editor outside it, or by a zone that may reach more. The third is the same concern one door along: that storage is a SQLite database extensions write to, so a row left by a run that may reach little must not be read by a run that may reach a great deal. All three are granted at exactly that depth, so a run can write its own and no sibling. A sandboxed editor therefore starts with its own settings, window state and extensions.

Language runtimes: node, python, go, rust, ruby, java, php, perl, elixir, bun, deno, swift, plus apple-toolchain-core. Each grants the interpreter, its package cache and its config.

The alternate node package managers pnpm and yarn are their own fragments. Each requires node — inheriting node itself, npm’s cache and the default registry — and adds only its own store and config, so pnpm on a pnpm-lock.yaml and yarn on a yarn.lock select exactly what that project uses. npm and corepack stay in node.

Pulled in by requires, not selected directly — except where a command name matches, as git does.

Each also declares the marker files that stand for it — rust on Cargo.toml, go on go.mod, python on pyproject.toml/requirements*.txt, and so on — so wardcell policy init can offer and pre-check the right toolchains for a checkout. The marker is offer_on, read only when a workspace is set up, never when a command runs.

What a coding agent or a toolchain needs from the rest of the machine. git, always-on for the command it names and pulling in the Apple command-line tools it shells out to; ssh, the agent-forwarding superset git does not get on its own; keychain, which the agents and the macOS applications resolve their sign-in through; and docker. All declare offer_on, so wardcell policy init offers them — and each offer_on names the picker group it belongs to rather than letting the folder decide. git and ssh sit under Integrations (and keychain with them); docker, gh and curl under Tools. git (.git) and docker (Dockerfile, compose.yaml) carry markers, so they are also pre-checked when a checkout holds one; gh, ssh, curl and keychain have no workspace footprint and so carry no marker — offered under show all but never pre-checked, since nothing in a checkout says a workspace drives them. gh in particular carries no marker deliberately — a .github/ directory means the repo uses GitHub Actions, not that this workspace drives the gh CLI. They are also still reached through requires, selected when their command runs, or added by draft as they are used.

git is also where linked worktrees are handled. A worktree keeps its objects and refs in the repository it was created from, so a grant on the working directory does not reach them — git status fails at fatal: not a git repository: (null). The path is a property of where a run starts rather than something a fragment can write down, so wardcell resolves it at launch and substitutes it as $GIT_COMMON_DIR. It is the one grant in this fragment that names a directory outside the workspace, and the fragment’s comment says what that hands over: a pointer naming a directory with no verifiable relationship to the workspace is refused, because .git is a file the run itself can write.

Pieces several fragments need, pulled in by requires and never selected on their own. electron-app is what every Chromium-based application needs before it will start under Seatbelt, and each apps/ fragment requires it; launch-services is the sandbox extension Chromium issues for its own bundle. agent-common is still a placeholder.

Terminal window
cat /etc/wardcell/policies/integrations-core/keychain.sbx
wardcell policy explain --profile agents/claude-code --os macos

They are short, commented text files. The same-line comment on a grant is the author’s reason for it — if a grant has no comment and is not obvious, that is worth questioning.

An upgrade replaces the bundled tree. Anything you edited in it is lost, so put your changes in your own fragments — see Adding your own fragments.