Skip to main content
One YAML file defines one judgment. A folder of specs is a project. hunch lint checks every key on this page; unknown keys are warnings, not errors. For completion and checks while you type, put this line at the top of a spec (the recipes start with it):
VS Code and Cursor read it through the YAML extension (redhat.vscode-yaml). The schema is also in the installed package, for working offline: python -c "import hunch, pathlib; print(pathlib.Path(hunch.__file__).parent / 'spec.schema.json')". It covers keys and allowed values; lint also checks what it cannot, such as whether a column reaches the spec.

A complete example

Every root-judgment key except where, chain, union, question, reviews and view. Passes hunch lint against a CSV with columns id, request, cwd, description, command, sample, gold_purpose, gold_destroys, gold_side_effects:
command_guard.yml

Top-level keys

judgment

Unique within a project; also the table name in the store. answers, traffic, and names starting with _ or sqlite_ are rejected.

model

The engine, pinned to an exact version. Names ending in latest or containing :~ are refused: different versions would share cache keys.
--model <engine> overrides it for one run. See Engines.

source

Where the rows come from. Relative paths resolve from the spec’s folder.
--source other.csv replaces a root spec’s source for one run; --traffic replaces it with rows logged by judge(...).

view

How traces(...) turns sessions into rows. Ignored for other sources.
  • next_message: the developer’s next message.
  • Injected text (IDE context, ! shell output, reminders, messages from other agent sessions) is removed from user messages. A message that is only injected text, a slash command, a context-continuation summary or an interruption is not a turn.
  • A session’s last turn has no next message, so it is not a row.
  • ran_after_edit: yes if the agent ran a command after its last edit, no if not, empty if it made no edits.
  • final_messages (runs): the agent’s last three messages.
  • commands: request is the last message the person typed before the command; cwd the folder it ran in; description the agent’s own note on it; rejected is person if the person refused the call, classifier if Claude Code’s auto-mode safety classifier did, no otherwise; failed is yes if it returned an error. After a context compaction, a background notification or a slash command (none of which count as requests), request is the last message the person typed before it. Ids are <session>#<turn>.<n>, the n-th command in that turn.

key

The column that identifies a row; diff, review and on_change pair rows by it. Must be unique (run warns on repeats). A ref(...) spec inherits its upstream’s key.

state

The columns the model sees, sent as a JSON object. Nothing else is sent. The cache key is computed from exactly this, after redact and clip; \r\n and \n give the same key.
One column name on its own is sent bare, as a string: the form a Pydantic AI agent sends its prompt in. The two forms are different inputs, so they have different answers and keys. Limit: about 32,000 tokens for state plus the longest question. run and compile warn above 80% and name the largest column; use clip.

redact

Rules applied to every state value before it is hashed, sent or logged. Named rule sets: Anything else is a regular expression; matches become [REDACTED].
Quote regular expressions with single quotes. In YAML double quotes, a backslash starts an escape, so "\d{6}" fails to load. A regex without backslashes, such as "[0-9]{1,3}([.][0-9]{1,3}){3}" for IPv4 addresses, works either way.

clip

N keeps a state column’s first N characters, -N its last N. The column must be in state; N is a nonzero integer.

where

Keep only rows where a condition holds, before asking. Can use source columns and, on a ref(...) spec, the upstream’s answer columns. Allowed: column names, constants, ==, !=, <, <=, >, >=, in, not in, and, or, not. Compared with a number, text is read as a number; an empty cell matches no numeric condition. lint rejects a column that does not reach the spec.

chain

Needs where. Confidence (<question>_p, and what act compares) becomes the judgment’s own confidence times the probability that where holds, computed from the upstream answers’ distributions across every hop.

union and question

Combines one question’s answers from several judgments into one table, so a tree of judgments is tested as one classifier. Every branch asks question with the same type; a row may reach only one branch (overlapping wheres stop the run). Rows gain _branch. A union has no model, source or questions.

reviews

The CSV where hunch review saves verdicts, which become gold for test and diff. Default: <judgment>.reviews.csv next to the spec. Specs asking the same question of the same rows can share one file.
A verdict applies while the row’s state is unchanged. Keep the file in git; it is not in the store.

weights

Corrects for a sample whose mix differs from production. Each row gets _w = population share ÷ sample share of its by value; test weights accuracy, calibration and estimates by it. Every by value needs a share; shares sum to 1. Set it on the spec that reads the file; downstream specs carry _w.

tests

Per-question thresholds for hunch test. A failure exits 1, unless the entry has severity: warn.
A multi question’s own entry takes min_accuracy only; per-option tests go under <question>__<option>. A metric takes its own tests under its name. Limits are shares between 0 and 1 (0.9, not 90). severity: warn in an entry makes its failures print WARN and leaves test’s exit code alone: for a check you want to see on every run but not block on. The default is error.

metrics

A metric is a rule over one row: a condition on the judgment’s answers and its columns, in the same language as where. In a rule, a question’s name stands for that row’s answer ('yes'/'no', an option, a level), with <question>_p and <question>_pyes beside it; source columns work too. hunch test counts the rule twice, and compares the two. This example is from the command guard, whose three yes/no questions are named destroys, reaches_outside and sends_out:
Why only random spot checks, and how gold stands in for an answer: How the numbers work. A rule that reads only columns, no answers, gets the first line only. test writes the same numbers to the results file under metrics.

examples

Statistical tests say how often a judgment is right; they cannot pin a case that must never go wrong. An example is a row you write in the spec, with the answers it must get. hunch test asks it like any other row and turns each example into one check.
The second example has severity: warn, so it is reported without failing test; without it, the line reads FAIL and test exits 1. An example is asked once and then comes from the store like any answer, so it costs nothing in later runs. It tests the spec’s own model: escalate is not applied, and where does not filter it. Results are in the results file under examples.

on_change

What happens to past answers when the spec changes:

description

A sentence saying what the judgment decides, shown by hunch docs. Never sent to the model, and not part of any answer’s key or the spec hash, so editing it re-asks nothing and doesn’t make a test stale.

exposures

What uses the judgment’s answers. hunch never calls them: hunch docs shows them, and hunch diff names the ones a change reaches. Like description, they are outside every key and the spec hash.

Answers an exposure relies on

uses as a mapping names, per question, the answers the exposure’s code compares against:
Lint, which every command runs first, fails while a listed value is not one the question can answer. hunch diff counts, per exposure, the rows whose answer moved into or out of a listed value. Change a spec safely shows both.

Question keys

Keys not sent are not in the cache key; changing them asks nothing.

Question types

One option. criteria maps each label to a description. Describe all options or none (lint warns on a mix). At most 255 options.

act

Confidence at or above which an answer is acted on; below it, the row goes to the review queue. A number in (0, 1], or for noul one threshold per answer:
With chain: true, the confidence compared is the chained one.

gold

The source column with the right answer. Empty cell: no gold for that row. Reviews override this column for the rows they cover and can accept two labels.

escalate

Re-asks answers below act on another engine. The new answer is used only if it clears act itself; both are stored and <question>_by names the engine used. Needs act and a different model.

none

Adds an option none_of_these with this description, so declining is an answer rather than low confidence. test reports how many rows declined.

Columns a judgment adds

A judgment’s output rows (what ref(...) and where read) have their input columns plus: lint rejects a question named like an input column (often its gold column). The store also adds <question>_key and _hunch_run_id; see Store.

YAML details

  • Only true and false are booleans. yes, no, on and off stay text, so options and act: {yes: …, no: …} keep their names.
  • Relative paths (source, reviews, files in traces(...) and py(...)) resolve from the spec’s folder.