# Configuring a guard

The binding schema in full — id, category, phases, detectors, mode, thresholds, severity bands, and error handling.

Source: https://tokentriage.com/docs/guardrails/configuring-a-guard

---
import Callout from '../../../components/Callout.astro';
import GuardrailBindingDiagram from '../../../components/GuardrailBindingDiagram.astro';
import NextSteps from '../../../components/NextSteps.astro';

A **binding** is one guard: a category, some detectors, the phases to run them on, and what to do
when a finding clears the gate.

```yaml
guardrails:
  enabled: true
  default_profile: standard      # applied to anonymous/keyless traffic; "" ⇒ off
  local_budget_ms: 50            # cumulative request budget for embedded detector work
  guards:
    - id: pii_in
      category: pii
      phases: [input]
      detectors: [pii.card, pii.email, pii.ssn]
      mode: monitor              # enforce | monitor
      threshold: 0.5             # minimum gate number a finding must clear
      severity_bands:
        - { min: 0.5, severity: low }
        - { min: 0.9, severity: high }
      ladder:
        low: observe
        high: redact
      on_error: open             # open | closed
      rule_ref: POL-PII-1
  profiles:
    standard: [pii_in]
```

<GuardrailBindingDiagram />

## The fields

| Field | What it does |
|---|---|
| `id` | the binding's name. It is what `engaged[]`, `fired[]` and `suspended[]` carry, and what you alert on |
| `category` | one of `pii` `secrets` `injection` `toxicity` `topical` `code` `multimodal` `format` `custom`. Every detector must belong to it |
| `phases` | `input` · `output` · `stream`. **Refused at boot** if a named detector does not declare the phase |
| `detectors` | the detector ids to arm. An unregistered id fails the compile |
| `mode` | `enforce` or `monitor`. `monitor` runs and discloses and **changes no byte** |
| `action` | one-rung sugar for `ladder: {low: <verb>}` |
| `ladder` | the real severity → verb table. Use it when you want different verbs at different scores |
| `threshold` | the minimum score a finding must clear before the ladder is consulted at all |
| `severity_bands` | the score cut-points that decide which rung a finding lands on |
| `on_error` | `open` (allow and disclose — the default) or `closed` (refuse unreleased traffic in enforcing mode); monitor preserves traffic |
| `rule_ref` | your auditable policy pointer. It rides every fire on the row |
| `local_budget_ms` | *engine-level*: the cumulative request budget for embedded detector work |
| `stream_fallback` | `monitor` (default) or acknowledged `closed` for missing stream coverage; `buffer` is recognized syntax but refused by stock gateway activation |
| `profiles` / `default_profile` | named sets of binding ids; the default applies to anonymous traffic. `""` means off |

## `action` or `ladder`, not both jobs

`action: redact` is exactly `ladder: {low: redact}`. With no `severity_bands` configured, **every
finding lands at `low`** — which is why the sugar works and why a one-rung binding needs neither
bands nor a threshold. Reach for `ladder` when you want a real table:

```yaml
      severity_bands:
        - { min: 0.5, severity: low }
        - { min: 0.8, severity: high }
      ladder:
        low: observe        # note it and move on
        high: redact        # splice it out
```

<Callout type="warning">
**Some detectors cannot use a ladder at all, and the reason is not configuration.** Where every
finding is `exact` with point 1.0 — `media.inventory` is the one shipped today — `threshold` is an
**on/off switch** and severity bands collapse to a single rung. The same is true of
`injection.judge`, whose verdict is a boolean, and of any detector while a **calibrated** artifact
licenses it: a calibrated point is a version-level aggregate, so every finding of that version
carries the same number and there is nothing left to discriminate. `guardrails explain` says so
beside the bound.
</Callout>

## `mode: simulate` is not a thing

```yaml
      mode: simulate     # ✗ refused by validation
```

> `binding "pii_in": mode 'simulate' is the off-path simulate API's stamp, never a configured mode`

Simulation is a property of a dry-run **report**, not of a policy. The dry run lives at
`tokentriage guardrails test` and `POST /tt/api/v1/guardrails/simulate`; see
[Proving it before you arm it](/docs/guardrails/proving-it-before-you-arm-it).

## The two settings that make you type out a consequence

### `on_error: closed` requires `fail_closed_ack`

`on_error` decides what happens when inspection cannot complete. Under `enforce`, `open` allows
traffic with a failure receipt; acknowledged `closed` refuses input or suppresses unreleased output.
Monitor mode preserves traffic and discloses the failure under either posture. See the
[failure matrix](/docs/guardrails/phases-and-streaming).

```yaml
      on_error: closed
      fail_closed_ack: "outage-over-leak, signed off by platform-security 2026-08-07"
```

Without the ack:

> `binding "pii_in": on_error 'closed' requires fail_closed_ack — a dead detector becoming an
> outage is a decision an operator must type out`

<Callout type="key">
Under enforcing mode, `closed` converts *a detector fault* into *an outage*, and that trade
belongs to somebody who will answer for it. `stream_fallback: closed` requires the same ack, for
the same reason.
</Callout>

### An external tier requires `external: allow`

An external judge requires destination consent before it can send request text. Consent alone does
not enable every registered detector: the stock gateway rejects sidecar bindings until their pinned
runtime is qualified, rejects external detectors that require raw content, and rejects spanless
external mutation. This example permits no judge spending and therefore makes no judge calls:

```yaml
guardrails:
  judge:
    external: allow                # the air-gap gate
    api_key_env: ANTHROPIC_API_KEY
    model: claude-haiku-4-5
    timeout_ms: 2000
    max_usd_per_request: 0         # explicit zero: deny before any judge call
```

<Callout type="warning">
**A positive judge money ceiling is refused by stock gateway activation while its pricer lacks a
proven conservative reservation bound.** Explicit zero is supported and denies calls before send.
The request-local external budget helper reserves before sending and settles actual known usage;
it shares money, call-count, byte and concurrency limits across that request. An unknown reservation
price under a hard money ceiling cannot authorize a call. Without a money ceiling, unpriced calls
remain disclosed as unpriced. See [Air-gap & limits](/docs/guardrails/air-gap-and-limits).
</Callout>

## Budgets, and what overrunning one costs

`local_budget_ms` bounds cumulative local inspection work for one request across input,
secondary preparation, output and stream phases; provider wall-wait time is excluded.
Exhaustion discloses `budget_exceeded` and applies the effective failure posture. An
acknowledged enforcing closed binding refuses release; monitor and open bindings retain
their documented behavior.

Measure the selected bindings on representative traffic before enforcing them. The sample
value of 50 ms is a configuration example, not a latency or coverage guarantee.

## Profiles

A profile is a named set of binding ids. `default_profile` is what anonymous or keyless traffic
gets; setting it to `""` means such traffic is unguarded.

```yaml
  profiles:
    standard: [pii_in]
    full:     [pii_in, pii_out, sec_in, inj_in]
  default_profile: standard
```

The resolved profile and the config hash ride every row as `policy_ref` — `standard@cfgc935358` —
so a row can always be traced back to the policy that produced it.

<NextSteps items={[
  { title: "Verbs & effects", href: "/docs/guardrails/verbs-and-effects", description: "What each verb does, which are reversible, and why the effect on the row is a different fact." },
  { title: "Proving it before you arm it", href: "/docs/guardrails/proving-it-before-you-arm-it", description: "Resolve, dry-run, monitor, enforce — and the rollback at each rung." },
  { title: "Guardrail vocabularies", href: "/docs/reference/guardrail-vocabularies", description: "Every closed enum this schema draws from." },
  { title: "Detector reference", href: "/docs/reference/guardrail-detectors", description: "Find the detector categories and choose a detector for a guard binding." }
]} />
