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):
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 minimal spec
This reads a CSV withid, message, and gold_urgent columns (yes or no). It asks one yes/no question and checks its answers against the known labels:
urgent.yml
noul is the YAML type name for a yes/no question. The engine returns both an answer and its probability. The gold column is optional when you have no answer key; you can build one with review.
A representative spec
This example shows several question types, routing thresholds, sampling weights and tests together. It is not an exhaustive spec: project keys such aswhere, chain and union appear below. It 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:yesif the agent ran a command after its last edit,noif not, empty if it made no edits.final_messages(runs): the agent’s last three messages.commands:requestis the last message the person typed before the command;cwdthe folder it ran in;descriptionthe agent’s own note on it;rejectedispersonif the person refused the call,classifierif Claude Code’s auto-mode safety classifier did,nootherwise;failedisyesif it returned an error. After a context compaction, a background notification or a slash command (none of which count as requests),requestis 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.
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].
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 (' 12 ' and 1e3 count); a cell that isn’t one (empty, n/a, nan) matches no numeric condition, != included, so not year > 2008 keeps it. -year reads the cell as a number the same way. Compared with True or False, true and false (any case) count too, as do 1 and 0; for yes write flag == 'yes'. in holds when == holds for some item of the list, not in when != holds for every one, so year in [2008, 2009] reads year as a number, and an empty cell is in neither. Against text, in looks for a substring and needs text on both sides: 2.5 in text matches nothing, not in included. A column on its own (where: flag) holds unless the cell is blank or flag == False would hold (false, 0); no is text, so it holds, and lint warns when a yes/no answer stands alone in a where, a metric rule or a baseline (where: recent: write recent == 'yes'). A where that keeps none of a run’s rows prints a warning. 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 branch may itself be a union that combines the same question. A row may reach only one branch (overlapping wheres stop the run). Rows gain _branch, the branch they came through. key defaults to the first branch’s and must be a column every branch has. A rule or a downstream where can read only the columns every branch has, even behind _branch == ...: a rule about one branch’s own column goes on that branch. A union has no model, source or questions: it takes question, key, reviews, metrics, tests, description, exposures and targets; lint says so for any other key.
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.
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 above 0; 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.
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. On a union, a rule reads its question, the columns every branch has and _branch. 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.
With
weights, every line is weighted to the population. Counts stay the rows seen; each rate is weighted, with a 95% interval on the effective sample size, which the line shows:
min_rate and max_rate check the weighted rate. test writes the same numbers to the results file under metrics.
Per group
by: <column> reports the metric once per value of that column, such as a party or a period. The column can be a source column or an answer of this judgment. The overall lines stay as they are.
on gold line). Unlike the unweighted overall line, a group’s rate on answers always has an interval: comparing groups is a claim about what each group produces, not only about these rows. With weights, group rates are weighted like the overall lines, and each line shows the effective sample size its interval uses.
Rows with no value in the column are left out and counted. A group under 10 rows is still listed, with a note that its interval is too wide to say much.
higher: [a, b] passes when the 95% interval of the difference, a’s rate minus b’s, lies above 0. It can pass while the two groups’ intervals overlap; test says so. It fails when either group has no rows.
The check is on the model’s answers. When both groups have gold, test prints the same difference on gold. In the example it does not hold there: the model misses more health bills before 2008, which widens the gap. A finding that passes on answers but not on gold is a finding about the model, until more gold says otherwise. One higher per metric: each extra comparison you run is another chance of a false pass, so pick the pair before you look.
Details: How the numbers work.
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.
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.
targets
Named ways to run the same spec. --target NAME picks one; without --target, the spec runs as written.
A flag given on the command line wins over the target:
--target dev --model X answers with X.
In an app, hunch.judge(spec, target="dev", ...) answers with the target’s model and store. HUNCH_TARGET sets the default for the CLI and for judge(), so a server runs as dev without a code change.
In a folder of specs, each judgment answers with its own targets.NAME.model, and a judgment without one uses its model. sample, store and max_cost apply to the whole run, so specs that set them must agree.
Lint fails on an unknown key inside a target, and on a --target that no spec defines. targets is not part of the spec hash, so adding one makes no test stale. test records the target in its results and hunch docs shows it. test --receipt refuses a target: a receipt records the spec as written.
Question keys
Keys not sent are not in the cache key; changing them asks nothing.
Question types
- choice
- noul
- score
- multi
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:
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 (behind a distilled: model, always); both are stored and <question>_by names the engine used. Needs act and a different model.
baseline
A rule that answers a yes/no question without a model: a keyword search, or a condition on columns. test scores it on the same gold rows as the model and prints both; with weights, accuracy, recall and precision are weighted, AUROC is not. When the rule is as accurate as the model, test says so.
A regex, found in any of the columns (yes when it is):
(?i). An empty cell matches nothing. Or a where expression, yes when it holds:
test writes the same numbers to the results file under baseline.
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 (whatref(...) 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
trueandfalseare booleans.yes,no,onandoffstay text, so options andact: {yes: …, no: …}keep their names. - Relative paths (
source,reviews, files intraces(...)andpy(...)) resolve from the spec’s folder.