Skip to content

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.sbx

The 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.

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.

They get used interchangeably elsewhere; here they mean different things.

Fragmentone .sbx file. The unit you write and review.
Profilea named fragment you select with --profile, usually a requires list.
Policythe merged result of resolving a root. What actually gets enforced.

So: you write fragments, select a profile, and wardcell enforces the resulting policy.

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"]

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.sbx

To add reach, write a fragment. Grants union, so yours composes with the bundled ones:

local-tools/my-tool.sbx
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:

local-tools/no-telemetry.sbx
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:

local-tools/claude-code-extra.sbx
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.

  • One concern per fragment — “what does this grant and why” fits in a sentence.
  • A same-line comment on every grant. wardcell policy explain shows 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 :*. Never allow_http "*" unless you mean it, and say why.

Check a fragment before relying on it:

Terminal window
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?

To iterate without sudo, point wardcell at a copy you own:

Terminal window
cp -R /etc/wardcell/policies ~/wardcell-policies
wardcell run --policy-root ~/wardcell-policies -- my-tool

This 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.

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.