Custom Linters in Practice: Sergo, Linter Miner, and LintMonster
gh-aw now registers 35 custom Go analyzers in
cmd/linters/main.go.
That linter surface is not maintained by hand alone.
It is grown, audited, and applied by three separate
workflows:
- Linter Miner proposes new analyzers from recurring patterns.
- Sergo stress-tests those analyzers for false positives, false negatives, and suppression gaps.
- LintMonster runs the custom suite and turns findings into tracked cleanup work.
The interesting part is not that each workflow exists. It is that they form a loop: one workflow adds lint rules, another challenges them, and a third drives the codebase toward compliance.
Linter Miner keeps adding new rules
Section titled “Linter Miner keeps adding new rules”The
workflow definition
is explicit about its job: mine discussions, issues,
and Go source, pick one new linter idea, implement it,
and open a PR. GitHub search currently shows a long run
of
[linter-miner] PRs,
and the recent examples are concrete:
fprintlnsprintfflagsfmt.Fprintln(w, fmt.Sprintf(...))and links to ADR 34498.timeafterleakcatchestime.After(...)insidefor+selectloops.errorfwrapvflagsfmt.Errorf(...%v..., err)where%wshould preserve the error chain.wgdonenotdeferredcatches non-deferredsync.WaitGroup.Done()calls.lenstringsplitrewriteslen(strings.Split(s, sep))tostrings.Count(s, sep)+1when the separator is provably non-empty.stringreplaceminusonerewritesstrings.Replace(..., -1)tostrings.ReplaceAll(...).
This is not a one-off burst. The same theme appears in
the blog’s own weekly updates:
May 25,
June 15,
and
June 22.
Those posts document fprintlnsprintf,
timeafterleak, errorfwrapv, and deferinloop as
shipped work rather than aspirational ideas.
Sergo pressure-tests the linters after they land
Section titled “Sergo pressure-tests the linters after they land”Where Linter Miner expands the rule set, Sergo does the adversarial follow-up. The workflow is focused on actionable Go analysis using Serena, and its issue history shows a steady pattern: find a precision gap, write a tightly scoped issue, and let the next PR harden the analyzer.
The clearest evidence is the issue-to-PR chain:
- Issue #40244
found that
errstringmatchonly handledstrings.Contains(err.Error(), ...); PR #40248 extended coverage toHasPrefix,HasSuffix,EqualFold,Index,LastIndex, andCompare. - Issue #41377
found missing
//nolint:support across four context-family linters; PR #41382 added suppression parity. - Issue #41376
found a false negative in
manualmutexunlockwhen two struct instances shared the same mutex field; PR #41383 fixed the keying model. - Issue #40947
found that
wgdonenotdeferredmissed goroutine closures launched inside loops; PR #41026 fixed the function-literal scope boundary. - Issue #41163
found that
lenstringsplitmishandled an empty raw-string separator; PR #41188 fixed the false positive and the broken autofix.
There is also useful evidence in the failures. Sergo’s Issue #40243 bundled several package-identity precision fixes into one direction, and PR #40247 closed unmerged after sprawling into a large branch. The narrower follow-up work still landed, including PR #40248. That is a good sign: the workflow is producing reviewable problems, not just optimistic reports.
LintMonster turns diagnostics into repository work
Section titled “LintMonster turns diagnostics into repository work”LintMonster
operates later in the loop. It runs
make golint-custom, groups findings by root cause,
creates or updates issues, and can assign up to three
Copilot agent sessions to fix them.
Its evidence trail is easy to follow:
- Issue #40932 grouped four resource-lifecycle and context-propagation findings; PR #41589 merged the targeted fixes.
- Issue #40933 tracked hard-coded path constants; PR #41611 replaced the flagged literals with existing constants.
- Issue #39314 established an authoritative function-length backlog for 653 findings.
- Issue #41466 refreshed that same backlog at 660 findings and kept it consolidated instead of spawning duplicate tracking issues.
This is what makes the custom linter suite operational instead of decorative. Rules only matter if they change the repository. LintMonster is the workflow that turns diagnostics into queues, slices, assignments, and merged cleanup work.
Why the three-workflow loop matters
Section titled “Why the three-workflow loop matters”Taken together, the workflows separate three jobs that usually get conflated:
- Invent a rule from a real pattern.
Linter Miner does this with new analyzers such as
timeafterleakandlenstringsplit. - Challenge the rule’s correctness. Sergo does this with issues such as #40947 and #41163.
- Apply the rule to production code. LintMonster does this with issue-to-PR chains such as #40932 → #41589 and #40933 → #41611.
That split is why the system looks durable. New rules keep arriving. Old rules keep getting corrected. The repository keeps absorbing the results.
Further evidence
Section titled “Further evidence”If you want to inspect the trail directly, start here:
- Source workflows: Linter Miner, Sergo, LintMonster
- Linter registry:
cmd/linters/main.go - ADRs: 34498, 39133, 40837, 41090, 41285
- Search views:
[linter-miner]PRs,label:sergoissues,label:lint-monsterissues
This is a useful pattern beyond gh-aw: treat static
analysis as a living workflow system, not just a binary
that runs in CI.