4.1. Linting
In one glance
- You will: Watch a static analyzer catch a bug the compiler happily accepted, then run the same gate your git hooks and CI run.
- You need:
mise run installfinished andmise run doctorpassing. No model, no network. - Time: about 15 minutes, hands-on.
Why an unchecked error survives the compiler and the tests
Static analysis is the gate that reads source for defects that compile. It catches an ignored error return, a leaked response body, an SQL handle left open, a malformed structured log. The two cheaper gates cannot see them: compilation answers whether the code can run, and the tests answer whether it does what the cases describe. Neither has an opinion about an error nobody read. This page makes one such defect visible by name, line, and column, then names the checker that owns every other file in the tree.
In this agent, every guarded write commits its state change and its audit row together or neither commits. That guarantee lives in one transaction helper, and it rests entirely on a single error being checked. Drop the check: it still compiles, every test that only asserts “no error came back” still passes, and a disk-full or lock-timeout commit failure is now reported as a successful restart.
golangci-lint is the aggregate that catches it: one binary running many independent analyzers over a package, among them errcheck, which flags a returned error nobody assigned or handled. Make that edit — the exercise at the end walks you through it and puts the file back — and run the aggregate:
cd agents/go
golangci-lint run ./data/...data/write.go:85:11: Error return value of `tx.Commit` is not checked (errcheck)
tx.Commit()
^
1 issues:
* errcheck: 1The line number, the column, and the caret under the offending expression are the whole review comment, delivered before anyone volunteered to read the diff.
How gofumpt, goimports, and golangci-lint divide the Go gate
mise run check is the same gate at every scope. In agents/go it format-checks the sources, runs the aggregate, and verifies the module manifests are tidy; at the repository root it fans out to all three Go modules plus the documentation, link, shell, workflow, license, and infrastructure checks. Your git hooks call those same tasks: formatters and offline checks on every commit, check:core and the tests before a push.
Treat the result as binary: a warning is a failed correctness gate, not polish you will get to later. The moment one warning is allowed to survive, the next one has cover.
goimports groups imports and applies base formatting; gofumpt adds a stricter, deterministic style on top; golangci-lint v2 checks that same formatter policy without writing, then runs the analyzers. Its configuration is deliberately explicit rather than clever: version: "2" selects the current schema, formatters enables goimports and gofumpt with this module’s import prefix and extra rules, linters adds security, error-handling, SQL resource, structured-logging, spelling, performance, and unused-code checks, and govet.enable-all: true turns on every applicable go vet analyzer inside the aggregate. Drop to raw go vet ./... while you are diagnosing one analyzer’s opinion, but keep mise run check as the gate that decides.
Known-vulnerability analysis is a different kind of question. mise run check:vuln runs govulncheck across the agents/go, evals, and tools modules, and it needs the network, so it stays off the commit hook and runs in the full root check and in CI instead. An offline commit must remain possible, and a stale or unreachable advisory database is not evidence that your dependencies are clean.
Which checker owns each surface, and what a suppression costs
Go is the loudest surface, not the only one. Each of the others has one owner, so a failure always names a tool you can run by hand:
| Surface | Primary checker |
|---|---|
| Go source and imports | gofumpt, goimports, golangci-lint v2 |
| Markdown, JSON, TOML, YAML | dprint |
| Course structure and includes | Repository convention checker |
| Shell | ShellCheck plus repository contract checks |
| Workflows | actionlint, offline Zizmor, pinned actions |
| Kubernetes and OpenTofu | Renderers plus configuration scanners |
Zizmor is a workflow security scanner; --offline keeps it from calling GitHub, and pinning means every third-party action is referenced by a commit SHA.
An aggregate is only worth its noise if every finding is actionable, so each linter enabled here answers a failure class this codebase has — the four this page opened with, plus an accidental copy. An analyzer that fires on nothing real teaches the team to skim.
When one of them is wrong, prefer changing the code. When it is wrong at exactly one site, scope the exception to that rule: a //nolint:rule // reason comment silences one rule on one line, and there are four such lines in this tree, each carrying both halves.
.golangci.yml also carries two exclusions considerably wider than that. Test files are exempt from gosec, because fixtures deliberately construct the hostile inputs production code has to reject. And eight gosec rules — G304 file paths, G204 subprocesses, and six more — are switched off for every package in the module, production included. Read that second list as a debt you can see, not as a policy to copy. Never add a suppression to force a green run: the guardrail you disable today is the one that was about to catch tomorrow’s incident.
8.5. Contributions takes the hook, the local run, and the hosted workflow apart. One detail belongs here, the only place the two CI jobs differ: the validation job installs the account-free tier — every dependency that needs no cloud account or API key — with install:validation, while the documentation workflow installs only its pinned documentation and browser toolchains before rendering and the browser checks.
Your turn: drop a commit error check and let errcheck catch it
Reproduce the opening capture on your own machine.
- Mode:
temporary experiment. - Goal: drop the commit error check in the guarded-write transaction, watch static analysis catch what the compiler waved through, then restore the file.
- Files to touch:
agents/go/data/write.goonly. Do not edit any test. - Preflight: require
git diff --quiet -- agents/go/data/write.go, then runcd agents/go && go build ./...andmise run checkgreen so a later red result is your edit and nothing else. - Steps: in
inWriteTransaction, replace the block that checks the result of committing with a baretx.Commit()followed byreturn nil. Predict first: will the compiler complain? Willcd agents/go && go test ./data ./tools -count=1go red? - Gate that proves completion:
cd agents/go && go build ./...succeeds, andgolangci-lint run ./data/...reports an unchecked error return value fromtx.Commitundererrcheckand exits non-zero. - Final state: run
git restore -- agents/go/data/write.go, thencd agents/go && mise run checkis green again.
Both predictions resolve the same way: the build succeeds and the focused run reports ok for both packages, because nothing in the suite arranges for a commit to fail. Only the analyzer had an opinion.
What you can do now
- You can say why the compiler and the tests miss an unchecked
tx.Commit, anderrcheckdoes not. - You can run
mise run formatandmise run checkfrom any module and read a warning as a failed gate rather than a suggestion. - You can name the tool that owns a
.go,.md,.sh, and workflow file, and say whycheck:vulnis not on the commit hook. - You can find every suppression this repository carries — four
//nolintlines with their reasons, and one module-widegosecexclusion list — and say what each one costs.
Keeping this gate’s findings at zero is what makes the next page’s red tests mean something: when the only noise is deliberate, a failure is information.
Continue to 4.2. Testing, where the question stops being “is this code well formed” and becomes “does it still do what it did yesterday”.