Skip to main content
A policy file describes one policy. Files live in policies/ in your repository, at any depth, with a .yaml or .yml extension.

A complete example

policies/hipaa-phi-egress.yaml

Top level

metadata

metadata.id is the identity of the policy. Renaming it in place means the old policy is removed and a new one created β€” which apply will refuse without --allow-remove.
Each id must be unique across your whole file set. Two files claiming the same id are rejected, because which one owned the resulting rows would otherwise depend on directory ordering.

spec

spec.appliesTo

environments and models are enforced at different moments, and the difference matters:
  • environments is checked once, when you apply. apply refuses to write a policy into an environment the policy does not list. It never fans one apply out into several.
  • models is checked on every request. A policy scoped to ["gpt-4*"] does not fire for a request reporting claude-opus-4, and a request that reports no model at all is out of scope rather than blocked β€” the same convention model_governance uses.

spec.rules[]

Rule order is significant and is preserved exactly as written. Valid type values: pii, toxicity, topic_boundary, format, cost_ceiling, custom_llm, regex, agent_rate_limit, tool_misuse, loop_detection, agent_misuse, scope_enforcement, delegation_control, model_governance, secret_egress, schema_contract.

model_governance

Globs use * only; everything else is literal. A caller that reports no model at all is out of scope for this rule rather than blocked by it.

secret_egress

The curated set covers AWS access keys, GitHub tokens (including fine-grained PATs), Slack, Stripe, OpenAI, Anthropic, Google, SendGrid, Twilio, npm tokens, PEM private keys, JWTs and bearer credentials. With action: redact the matched material is replaced with [REDACTED] rather than the response being blocked.

schema_contract

Output that is not valid JSON is reported as such rather than as a schema mismatch. A rule with no schema passes rather than failing every response β€” the misconfiguration is caught by zespan policy validate.

spec.exemptions[]

An exemption suppresses the policy’s rules for the scope it names. A suppressed rule produces no guardrail event at all β€” not one recorded as allowed, which would read on the dashboard as β€œthe control ran and found nothing”.
expiresAt is required, and a policy whose exemption has already expired fails validation β€” naming the exemption’s reason so you know which one to renew. An exemption with no expiry is a permanent hole in a control that nobody ever revisits.
The expiry is checked on every request, not when you apply. The day named in expiresAt is itself still exempt (compared as a UTC date, so the result does not depend on your server’s timezone); from the day after, the control enforces again on its own β€” you do not have to re-run apply for it to take effect. Renewing an exemption, or changing appliesTo.models, is a real change to the policy, so zespan policy plan reports it as an update.

The supported YAML subset

Policy files are parsed as YAML 1.2 core. Almost everything you would normally write works, including block scalars, flow collections, comments, and any consistent indentation. A small set of features is rejected β€” each with the line number and a message saying what to write instead. These are all things that make a security policy harder to read or review, not parser limitations:
Because parsing follows YAML 1.2 core rather than 1.1, on, off, yes and no stay strings rather than becoming booleans. This is the well-known β€œNorway problem” (no β†’ false), and avoiding it matters here because on is a plausible value in a policy file.

Validation

Runs offline. Every problem in every file is reported at once:

Next steps

Policy as code

The authoring loop and the ownership model.

zespan policy

Every verb and flag.