The Policy Root
A policy root is a directory containing base.sbx and a tree of
fragments. The installer puts one at /etc/wardcell/policies/.
/etc/wardcell/config.toml root-owned; settings for wardcell itself/etc/wardcell/zones.toml root-owned; optional — turns on Zones/etc/wardcell/policies/ root-owned; every sandbox is built from this├── base.sbx always loaded, always first├── agents/ claude-code.sbx, codex.sbx, pi.sbx├── apps/ macOS app bundles├── toolchains/ node.sbx, python.sbx, go.sbx, ...├── integrations-core/ git.sbx, ssh.sbx, keychain.sbx, worktrees.sbx└── shared/ pieces several fragments require
~/projects/app/ a workspace of yours└── .wardcell.sbx optional — adds what a profile is missing, never weakens base.sbxThe policy root is resolved in this order: --policy-root, then
$WARDCELL_POLICY_ROOT, then /etc/wardcell/policies/. An organisation can turn
the first two off in config.toml. Layer
3 is the .wardcell.sbx in the directory a command runs in —
see Workspace Policies.
Why /etc/wardcell
Section titled “Why /etc/wardcell”Most developer CLIs keep their configuration in your home directory —
~/.config/gh, ~/.cargo, ~/.npmrc. The policy root is different, because it
is not configuration. It is the security boundary.
A sandbox only protects you if what it is built from cannot be changed by the
things it protects you from. Anything in your home directory can be rewritten by
any process running as you — a malicious postinstall script, a compromised
editor extension. If the policy lived there, any of those could add
allow_read ["~/.ssh"] today and every sandbox tomorrow would honour it,
silently, because the policy is working exactly as written.
/etc/wardcell/ is owned by root and writable by nobody else. Changing it takes
an administrator password, which puts a person — or your configuration
management — in the path of every change. That is the same reason sudoers,
SSH daemon configuration and endpoint security agents keep their policy there. A
sandboxed process can neither read nor write any of it: base.sbx denies
/etc/wardcell inside the sandbox, so an agent cannot learn what the policy
allows or change what its next run gets.
/etc is used on both platforms so one policy root works everywhere. On macOS
it is /private/etc, the real directory /etc links to.
Three words
Section titled “Three words”They get used interchangeably elsewhere; here they mean different things.
| Fragment | one .sbx file. The unit you write and review. |
| Profile | a named fragment you select with --profile, usually a requires list. |
| Policy | the merged result of resolving a root. What actually gets enforced. |
So: you write fragments, select a profile, and wardcell enforces the resulting policy.
Subdirectories are for humans
Section titled “Subdirectories are for humans”The resolver does not care about them. A requires entry is a path relative to
the root with .sbx omitted — there is no kind field and no registry. Move a
fragment, update the paths that name it.
requires = ["toolchains/node", "integrations-core/git"]Adding your own fragments
Section titled “Adding your own fragments”Drop a .sbx file anywhere under the root; it takes effect when something
selects it — selects_on, --profile, or another fragment’s requires.
Writing there needs sudo, by design (see Why /etc/wardcell).
Never edit a bundled fragment: an upgrade replaces the files wardcell ships and
removes the ones it stops shipping, but leaves everything else — your fragments
and your zones.toml — alone.
/etc/wardcell/policies/├── base.sbx ← bundled├── integrations-core/ ← bundled└── local-tools/ ← yours └── my-tool.sbxTo add reach, write a fragment. Grants union, so yours composes with the bundled ones:
fragment "my-tool" { selects_on = { commands = ["my-tool"] } requires = ["toolchains/node"] # reuse, don't rewrite
fs { allow_read ["~/.config/my-tool"] } # tool config net { allow_http "api.my-tool.example" { # the tool's API allow_requests ["GET /v1/**", "POST /v1/jobs"] } }}To remove reach, add a deny. It wins from anywhere, so you never have to find the fragment that granted it:
fragment "no-telemetry" { net { deny_hosts ["telemetry.example.com:*"] }}Something has to select a deny-only fragment too — give it a selects_on, name
it in a requires, or put it in a profile.
To extend a bundled fragment, compose rather than fork. A second fragment
with the same selects_on applies alongside the bundled one, which keeps
upgrading cleanly:
fragment "claude-code-extra" { selects_on = { commands = ["claude"] } # applies alongside the bundled one requires = ["integrations-core/keychain"]
net { allow_http "api.internal.example" { allow_requests ["* /**"] } }}Reusable pieces need no selects_on — a fragment that others require
applies only when named. A role fragment is exactly this pattern with nothing
but a requires list.
Keeping fragments reviewable
Section titled “Keeping fragments reviewable”- One concern per fragment — “what does this grant and why” fits in a sentence.
- A same-line comment on every grant.
wardcell policy explainshows the trailing comment and nothing else, so a reason written in a paragraph above the rule reaches nobody. The fragments wardcell ships are linted for this (make lint-policies); your own root is not, but the next reader still is. - The narrowest thing that works —
GET /v1/**over* /**, a named port over:*. Neverallow_http "*"unless you mean it, and say why.
Check a fragment before relying on it:
wardcell policy verify # does it parse and merge?wardcell policy explain -- my-tool # is the rule in effect?wardcell policy explain -- my-tool --os linux # on both platforms?wardcell run -- my-tool # does it actually work?Developing a policy
Section titled “Developing a policy”To iterate without sudo, point wardcell at a copy you own:
cp -R /etc/wardcell/policies ~/wardcell-policieswardcell run --policy-root ~/wardcell-policies -- my-toolThis is for writing and testing, not everyday use: a root you own has none of
the protection above. When it works, install it into /etc/wardcell/policies/.
On a machine whose config.toml sets policy_root.allow_override = false,
--policy-root is refused.
Sharing across a team
Section titled “Sharing across a team”A policy root is a directory of text files, so it diffs and reviews like code.
Distribute the whole root as one artifact — config management, a package, a git
checkout — and install it into /etc/wardcell/policies/. A root a user can write
is one any process running as that user can widen, so if your configuration
management owns that directory, set policy_root.install_bundled = false in
config.toml so an upgrade never changes
it, and policy_root.allow_override = false so the CLI builds from nothing else.
Once distributed, base.sbx is the floor nobody can override. A workspace’s
.wardcell.sbx can add to a profile but never weaken it, and
workspace_policy.mode = "off" removes it altogether. See Workspace
Policies and, for a hard privilege floor,
Configuring Zones.