policies/ in your
repository, at any depth, with a .yaml or .yml extension.
A complete example
policies/hipaa-phi-egress.yaml
Top level
metadata
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:
environmentsis checked once, when you apply.applyrefuses to write a policy into an environment the policy does not list. It never fans one apply out into several.modelsis checked on every request. A policy scoped to["gpt-4*"]does not fire for a request reportingclaude-opus-4, and a request that reports no model at all is out of scope rather than blocked β the same conventionmodel_governanceuses.
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β.
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
Next steps
Policy as code
The authoring loop and the ownership model.
zespan policy
Every verb and flag.

