---
title: "Check classified text"
description: "Understand classification matching and refusal behaviour."
canonical_url: "https://docs.oz0.ai/build/plugins/verified/guardrails/classified-text"
---
# Check classified text

> Understand classification matching and refusal behaviour.

For: tenant administrators and plugin users.

Understand classification matching and refusal behaviour.

## Classified text

A value that carries a security-classification marking of the `classified.core` pack is refused before a model reads it: a job whose input is marked does not start, and a tool's result that is marked reaches the model as a refusal ("withheld: ... it is marked 1 eu restricted, and classified text never reaches a model here"). Nobody is asked, since a person's yes would pass the text on to the model. The reason names the marking, never the text.

The check runs first, at `job_start`, `post_tool_call` and `job_end`; when it cannot run in time, or a value is longer than it reads (16 MiB), the value is refused. A pack of kind `classified` (your own, or a sector pack) adds markings; its only action is `refuse`.

The check reads keys as well as values, and reads the text folded first: zero-width characters, soft hyphens and other
invisible format characters are dropped, and fullwidth letters, non-breaking spaces and letters written with a
combining mark read as their plain forms (Unicode NFKC). So `HEM` and `LIG` with a zero-width space between them, or a
stamp from a PDF whose words a line break or a non-breaking space separates, is refused like the plain stamp.

Besides plugin tools' results, the `post_tool_call` hooks see the results of the built-ins that bring text from outside
the job: a person's answer (`oz.ask_user`), an event's payload (`oz.wait_for_event`), another job's answer
(`oz.ask_colleague`) and the job tree's board notes (`oz.board_read`). Each is checked in the job that reads it, by
that job's agent's rule.

`GUARDRAILS_CONFIG` cannot turn this off: `"packs": {"classified.core": false}` is a setting error (the hooks then fail
closed, as for any setting they cannot read), and `pii: off` does not touch it. When the other settings do not parse,
the classified check still runs on the baseline pack.

The one loosening is for agents whose data stays on the node: an agent with
[`data: local`](https://docs.oz0.ai/fleet/local-models#name-a-local-model), on which the platform refuses every model that is not the node's own.
It holds only for agents whose model calls the runtime makes itself: a harness or remote agent's session reaches
models on its own, so its jobs are refused as any other's. A tenant
admin may allow a marking for such agents by its ID (the pattern IDs: `se_kvalificerat_hemlig`, `se_hemlig`,
`se_konfidentiell`, `se_begransat_hemlig`, `de_streng_geheim`, `de_geheim`, `de_vs_vertraulich`, `de_vs_nfd`,
`eu_top_secret`, `eu_secret`, `eu_confidential`, `eu_restricted`):

```bash [Terminal]
oz0 setting set GUARDRAILS_CLASSIFIED_LOCAL '["se_begransat_hemlig"]' \
  --reason "the archive's local model reads BEGRÄNSAT HEMLIG memos"
oz0 setting delete GUARDRAILS_CLASSIFIED_LOCAL --reason "no more"
```

It is a [governance change](https://docs.oz0.ai/govern/reviewed-changes#four-eyes-for-governance-changes): it needs the governance-write permission besides settings-write and a reason (`--reason`, or `$OZ0_REASON`; `--decision-ref` for a case number), the audit log keeps both, and allowing a marking waits for a second admin where the tenant has four eyes on (the command prints the change request's ID). Through the admin API, the reason and the decision reference are the `reason` and `decision_ref` fields of `SetSettingRequest` and `DeleteSettingRequest`, and a change that waits comes back in `SetSettingResponse.change_request`; the console's plugin settings ask for the reason when the name is this one.

Taking a marking back, or deleting the setting, applies at once. An allowed marking passes into the job's input and its tools' results for a `data: local` agent only; every other agent is still refused, and so is every job's output, since an output goes on to people, apps or a parent job's model.

Nothing of a `data: local` agent's job reaches a model behind the edge, so the allowance never sends allowed text there
either: a tool that asks a model of its own (`uses_model`) gets no model key in such a job and answers that it cannot ask
one, and the [intent check](https://docs.oz0.ai/build/plugins/verified/guardrails/intent-and-limits#the-intent-check) asks no model for such a job and sends the call to a person instead. A
note a local agent writes to the board, or an answer it gives a colleague, is checked again in the job that reads it:
a cloud agent's job refuses the allowed marking there.

Setting `GUARDRAILS_CLASSIFIED_LOCAL` needs `governance.write` besides `settings.write`, so a custom role with
`settings.write` alone sets every other setting and not this one; a tenant secret of the same name never becomes the
setting, nor reaches nodes as one.

What it does not catch: markings written another way (lower case, spelled out, split by a hyphen or across fields, in
an image or a file), another country's classes than those listed, and text the hooks do not see (a harness session's
own tools and its built-ins, anything a tool sends elsewhere itself). The marking says the text is classified; the
check does not judge whether it is, and whether anything may let classified text reach a model is not something this
page answers.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
