Skip to content

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.

Terminal window
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.

Every command is a single checks.Command value registered at init time, and that one declaration is the source for all of:

SurfaceGenerated by
lookout <cmd> --helpCommand.Help(), at runtime
The MCP tool schemainternal/mcpserver, at runtime
Skill referencesdev/tools/gen-skill-refs (committed; a drift test fails if stale)
These reference pagesdev/tools/gen-site-docs (committed; same)
The finding-kind glossaryRegistry.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.

Four things, each with a test that fails until you answer:

  1. 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.
  2. Whether a bare lookout scan runs 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.
  3. RBAC, if the check reads an API resource nothing else reads. A test parses the deployed ClusterRole against the declared requirements.
  4. 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.
  • 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”.
  • scanned is 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.