Entry Approval
enter_from says which zones may enter a zone. Approval decides whether a
particular entry happens — this command, from this chain, in this directory, now.
It takes two halves, in two files, because they belong to two different people:
[zones.release]profile = "release"enter_from = ["work"]requires_approval = true[plugin]path = "/usr/local/libexec/wardcell/wardcell-plugin"The policy root is the artifact an organisation distributes and versions; the chat integration or ticket system behind an approval is a property of the fleet it is deployed to. Split this way, one policy root serves machines wired to different approvers.
One plugin serves every gated zone — the question names its target, so a plugin
that holds different zones to different rules does that itself. A machine not set
up to ask does not start the run at all: a policy root with a gated zone and no
plugin that implements approvals.zone fails at startup, rather than running
and refusing every entry it was asked about.
It can only narrow
Section titled “It can only narrow”The plugin is asked after enter_from has already allowed the entry. So it
can refuse entries the policy permits, and it can never permit one the policy
does not. A plugin that approves everything leaves the policy exactly as it was —
which is what makes it safe to add to a zone that is already correct.
Waiting for a person
Section titled “Waiting for a person”An approval that goes to a human comes back when it comes back, so a question and
its answer are separate messages and nothing holds a process open in between.
approval_timeout is optional:
[zones.release]requires_approval = trueapproval_timeout = "4h" # omit it and the entry waits as long as it takesOmitted, the entry waits indefinitely. That is the right default for a release gate and a real cost to understand: the sandbox, its entry lock and the user’s terminal are held for the whole wait, and that sandbox cannot enter another zone meanwhile. Ctrl-C still works, and the wait ends immediately if the plugin crashes or the run ends.
Approve once, or stop being asked
Section titled “Approve once, or stop being asked”An agent that needs a gated zone needs it more than once, and a person asked the same question six times stops reading it. So an approval says how far it reaches — the two buttons a chat prompt would offer:
| once | this entry only. The default |
| always | this entry and every later entry into the same zone with a command line the approval covers, for the rest of the run. The plugin is not asked again |
A standing approval is keyed on the target zone and on the words the command line
must start with. By default those words are the whole command line that was
approved, so approving npm publish into release does not approve npm install
into release, and does not approve anything into staging.
The plugin may say it covers fewer words — ["npm", "install"] for an entry
running npm install left-pad@1.1, so the next package is not another question.
It can only shorten the command line it was asked about, never name a different
one, so an approval can widen as far as “any npm command in this zone” and never
reach a program nobody was asked about. A chat prompt with three buttons is the
shape this is for — Approve once, Always npm install …, Always npm … —
so what someone grants is what they were shown. Which split makes sense is a
question about the tool being run, which is why wardcell does not guess it: the
plugin says so in covers.
It is not keyed on which zone the request came from, because enter_from has
already permitted every chain that can reach the zone — keying on that as well
would narrow it to whichever escalation path happened to ask first. The directory
cannot vary: it is where the run was started.
wardcell remembers it, and only for the life of the run. Nothing is written to
disk, so a later run asks afresh and there is no stored permission to go stale.
Every entry is still audited, with "standing": true on the lines that were not
asked about, alongside the request_id, by, reference and covers of the
approval they rest on.
There is no refusing “always”. A refusal that stood would turn one mistaken click
into a run that cannot enter the zone at all, with nothing to undo it; a policy
that should never allow an entry says so in enter_from, where it outlives the
run.
What the person sees
Section titled “What the person sees”$ wardcell zone enter release -- npm publisherror: entering zone 'release' was not approved: not during the release freezeExit code 1, the same as a policy denial — but the message is different on
purpose. A denial means asking again will not help; a refusal means it might.
Everything that goes wrong refuses
Section titled “Everything that goes wrong refuses”A plugin that will not start, a connection that drops, an unreadable answer, an
approval_timeout that passes, or no plugin configured at all: all refuse.
This is the whole design. An approval gate that opened whenever the thing guarding it was broken would be worse than no gate, because the organisation would believe it had one.
In the run log
Section titled “In the run log”An entry into a gated zone is an audit.decision event in the run’s
log, recording what was asked and what came
back alongside the decision:
{"ts":"2026-10-08T14:22:03.512Z","run":"9f2c…","level":"info","source":"supervisor","category":"audit","event":"audit.decision","msg":"release: allowed","data":{"attempt_id":0,"calling_zone":"work","chain":["work"],"target_zone":"release","command":["npm","publish"],"decision":"allowed","approval":{"request_id":"9f2c…-0","approved":true,"by":"alice@example.com","reason":null,"standing":false,"reference":"slack:C0ABCDEF/p1700"}}}Read a run’s decisions on their own with
wardcell logs show <id> --category audit.
The decision is allowed, denied (enter_from said no, and nothing was asked)
or refused (the approval said no). reference is the plugin’s own name for the
request — the thread or ticket an auditor would go and read. standing is true
when the plugin was not asked at all, because an earlier approval in this run said
it covers this zone and command; the other fields then repeat that approval, so
the line is still attributable to the person who gave it. Where two standing
approvals both reach an entry, it is recorded against the narrower one — “alice
approved every npm install” is a truer account of why an entry happened than
“bob approved everything”. For a zone that does not require approval, approval
is null.
Writing a plugin
Section titled “Writing a plugin”An approval plugin does not have to ask anyone — it has to decide, and a rule about office hours or a command allowlist needs no human in it at all.
Start from an example, in Rust or Go, which implements the protocol and leaves you one function. Approval Plugins is the protocol, the waiting rules and the failure modes in full.