Adding a check
A check is one read-path command: lookout audit netpol, lookout state volumes, lookout triage delta. Adding one is a scaffolder invocation plus
four decisions.
The full walkthrough — one real check taken end to end, with the rationale for
each part of the declaration — lives in the tree at
docs/adding-a-check.md,
next to the code it describes. This page is the orientation.
Start with the scaffolder
Section titled “Start with the scaffolder”dev/tools/new-check --group=state --check=quotas \ --summary="When creates are rejected with 'exceeded quota': which ResourceQuotas are at their limit ..."That writes the command, its test suite and its first golden, registers it, and runs the generated golden test to prove the scaffold compiles. What comes out is a working command that finds nothing — the job is to turn it into one that finds something, and only something.
The scaffolder reads the group package before it writes: a group whose Deps
carries a Kubernetes client gets the client guard and a fake-clientset fixture,
one that carries only a clock gets neither. A group that does not exist is
refused rather than created, because a group is a claim about a class of
question and that is not a template decision.
One declaration, five surfaces
Section titled “One declaration, five surfaces”Every command is a single checks.Command value registered at init time, and
that one declaration is the source for all of:
| Surface | Generated by |
|---|---|
lookout <cmd> --help | Command.Help(), at runtime |
| The MCP tool schema | internal/mcpserver, at runtime |
| Skill references | dev/tools/gen-skill-refs (committed; a drift test fails if stale) |
| These reference pages | dev/tools/gen-site-docs (committed; same) |
| The finding-kind glossary | Registry.KindGlossary() |
Nothing is written twice and nothing needs keeping in sync — but a field left empty is a hole in five places at once, so the registry validates the declaration and panics at init on an invalid one.
What the generator will not decide
Section titled “What the generator will not decide”Four things, each with a test that fails until you answer:
- The claim itself — the rationale comment, the finding kinds, the output glossary, and the detector. Every rule worth writing has a legitimate look-alike; naming it and excluding it is most of the work.
- Whether a bare
lookout scanruns it. Every registered command is either in scan’s default stage or in its exclusion table with a recorded reason. A coverage test fails until one of the two is true. - RBAC, if the check reads an API resource nothing else reads. A test parses the deployed ClusterRole against the declared requirements.
- Skills, if a workflow should reach for the command. A command no skill names is reachable by an agent that already knows it exists, and by no other.
The house rules
Section titled “The house rules”- Silent when healthy. Healthy resources are omitted, and every invocation
ends with
scanned=<n> findings=<n> elapsed=<d>so “cluster healthy” is never confused with “wrong flag”. scannedis what was examined, not what was found. It is the denominator of the coverage claim.- Exit 2 is a usage error, exit 1 is a runtime one. Exit 1 is the one a caller retries.
- Findings carry a fingerprint — class-level for posture claims, instance-level for incidents.
- Goldens update one way,
UPDATE_GOLDEN=1 go test ./pkg/checks/<group>.
Then dev/tools/gen-skill-refs, dev/tools/gen-site-docs (last, or its output
is stale), and dev/tools/ci.