Skip to main content
Policy as code lets you keep your guardrail policies in your repository instead of only in the dashboard. You author YAML files, review changes the same way you review any other code, and apply them with a planapply loop. The dashboard remains fully usable. A project can mix both: the platform team keeps critical controls in git, while a compliance lead tunes others from the UI. Neither surface can silently clobber the other.

Why you might want this

Review before enforcement

A change to what gets blocked in production goes through the same review as any other change, with a diff a reviewer can read.

Reproducible across environments

The same files apply to staging and prod, so the two cannot drift apart by accident.

An audit trail that proves itself

Every apply records the content hash of each file, so an auditor can see exactly what was deployed and when.

Catch mistakes before they ship

zespan policy validate runs offline as a pre-commit hook — no network, no API key.

The authoring loop

1

Scaffold a policy

Creates policies/pii-egress.yaml with a starter policy in dryrun mode.
2

Validate locally

Runs entirely offline — it needs no API key and makes no network call, which is what makes it safe as a pre-commit hook. It reports every problem in every file at once, rather than making you fix one line and re-run.
3

See what would change

Prints the change set without changing anything:
4

Apply

apply always computes a fresh plan first and sends its hash along, so you can never apply a plan you did not see. If the state changed in between, the apply is refused rather than silently applied to different state.

Ownership: what code owns and what the dashboard owns

Every guardrail row carries an ownership marker. This is the mechanism that makes mixing the two surfaces safe.
policy apply only ever updates or removes rows it owns. A first apply from a directory containing one file, run against a project with twenty dashboard-authored guardrails, will create one row and leave the other twenty untouched. Pruning is bounded by ownership, never by which files happen to be present.

Detaching a policy

Sometimes you need to change a policy from the dashboard — during an incident, say, when editing a file and opening a PR is too slow. Open the guardrail’s detail page and choose Detach. Ownership moves to the dashboard, the form becomes editable, and the next zespan policy apply reports the policy as a conflict and refuses to overwrite it:
That refusal is the point. The alternative — silently re-applying the file over someone’s emergency fix — is the failure mode this design exists to prevent. When you are ready to move the change back into the file, edit the file to match and run zespan policy apply --force to take ownership back.

Removing a policy is a hard stop

If a policy disappears from your file set, apply refuses:
Removing a guardrail removes a control, and a control that vanishes silently is a coverage gap discovered during exactly the incident it existed to prevent. --allow-remove makes it deliberate.

The enforcement ladder

spec.enforcement sets the policy’s posture. It applies to every rule in the policy, so you can move a whole policy along the ladder in one line:
Start every new policy at dryrun. Look at the volume it would have caught in the Guardrails log, then move to warn, then deny. Going straight to deny on a rule you have never measured is how a policy takes down production traffic.
Note that warn also softens redact — a policy explicitly placed in warn mode must be observable without changing what your application receives.

Governing models, secrets and output shape

Three primitives exist specifically for policy files, beyond the content guardrails you may already use:

model_governance

Allow/deny model globs, a required provider, and data-residency regions.

secret_egress

Credentials and tokens in output, matched against a maintained pattern set.

schema_contract

Output validated against your own JSON Schema, not just “is it JSON”.
Two behaviours worth knowing, because both fail loudly rather than quietly:
  • If requireProvider is set and the provider cannot be determined from the model name, the rule fails rather than guessing. A guess would silently admit a model nobody vetted.
  • If allowRegions is set and the caller did not report a region, the rule fails. Passing would report a data-residency control as satisfied when it was never actually evaluated.

Scoping a policy to environments

appliesTo.environments is a guard, not a router. It does not expand one apply into several, and it never sends a policy anywhere you did not name on the command line. It does exactly one thing: apply refuses to write a policy into an environment the policy does not list.
The alternative reading — treat the list as a set of targets and apply to all of them — is how a policy reaches production without anyone intending it. Naming the environment on the command line stays the only way anything is written:
The check compares against the resolved environment. --env production resolves to the prod environment, so a policy listing prod applies cleanly under either spelling; omitting --env resolves to the project’s default environment and is checked against that. A policy that lists no environments is unconstrained and applies wherever you point it. Nothing is written when the guard fires — this refusal is checked before every other one, so you hear about a misdirected apply before you hear about a stale plan.

Scoping a policy to models

Unlike environments, this one is checked on every request: the policy’s rules simply do not fire for a request reporting a model outside the list. Globs use * only. A request that reports no model at all is out of scope rather than blocked — passing an unvetted request through a model-scoped control would be a worse default than skipping it, and model_governance already established the convention.

Scheduling

A policy can apply only during certain days and hours:
Outside the window, the compiled rules are applied in a disabled state. Only timezone: UTC is honoured today; any other value falls back to UTC rather than using the server’s local offset, which would make the same policy behave differently depending on where it ran.

Adopting a project that already has guardrails

You do not have to hand-write a file for every guardrail you already built. zespan policy pull generates them from what is already there:
1

Generate the files

Writes one file per guardrail into policies/, and prints a warning for anything it could not express exactly (see below). It never overwrites a file you have already edited unless you pass --force.
2

Check that it round-trips

This should report no changes to make — only pending adoptions. That is the point of generating rather than transcribing: the files and the live state agree by construction, so any drift you see later is real drift and not an artifact of adoption.
3

Take ownership

Each guardrail becomes code-owned in place — it keeps its id, so its execution history and anything referencing it survive the handover. From here it is read-only in the dashboard, with the usual Detach escape hatch.
--adopt is required for the same reason --allow-remove is: taking over a guardrail makes it read-only for whoever built it in the dashboard, and that should be a decision rather than a side effect.

What pull cannot express

Generation is not lossless, and it says so at the time rather than letting you discover it later:

Reviewing changes in the dashboard

Guardrails → Review changes does what zespan policy plan does: paste or upload policy files and see the same change set, computed by the same endpoint, so the two cannot disagree. It is deliberately read-only. Reviewing needs policy:read, which every role has; applying needs policy:apply, which only owner and admin have. Somebody reviewing a pull request should not need a terminal, and should not gain the ability to apply by using the dashboard instead.

Before you enforce

Promoting a policy to deny requires a backtest or an explicit --untested. zespan policy test runs the policy over your own recorded traffic and shows what it would have caught and what it would have broken — see Policy testing. Policies can also be evaluated in-process by the SDK rather than over the network, for the rules that are pure functions — see Local evaluation.

What is not in this release

  • Compliance packs — installable HIPAA / EU AI Act / PCI / OWASP-LLM policy sets.
  • Custom expressions and webhooks — CEL predicates and customer-hosted evaluation endpoints.

Next steps

File reference

Every field, its type, and whether it is required.

zespan policy

The full CLI verb reference.

Guardrails

The dashboard side, including the code-managed badge and Detach.

CLI overview

Configuration precedence for API keys and project ids.