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.
base.sbx
Section titled “base.sbx”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$WORKDIRand$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.
agents/
Section titled “agents/”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.
wardcell run -- claude # picks up agents/claude-code.sbxcodex 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:
wardcell run -- /Applications/Claude.app # picks up apps/claude-app.sbxwardcell run -- /Applications/Codex.appwardcell 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.
toolchains/
Section titled “toolchains/”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.
integrations-core/
Section titled “integrations-core/”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.
shared/
Section titled “shared/”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.
Reading a fragment before trusting it
Section titled “Reading a fragment before trusting it”cat /etc/wardcell/policies/integrations-core/keychain.sbxwardcell policy explain --profile agents/claude-code --os macosThey 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.
Upgrades
Section titled “Upgrades”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.