You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This exact bug was filed as #56533 by a prior Sergo run and auto-expired unfixed on 2026-09-03 (state_reason: not_planned) - the self-expiry checkbox lapsed with zero code changes. Re-verified today (2026-09-05): both the production bug and the linter blind spot that hides it are still present, unchanged.
Confirmed still-broken production code
pkg/cli/audit_run_pipeline.go:317-325:
funccacheRecoveryError(messagestring, runIDint64, runOutputDirstring, errerror) error {
returnfmt.Errorf(message+"\n\n"+"To download artifacts, use the GitHub MCP server:\n\n"+"1. Use the github-mcp-server tool 'download_workflow_run_artifacts' with:\n"+" - run_id: %d\n"+" - output_directory: %s\n\n"+"2. After downloading, run this audit command again to analyze the cached artifacts.\n\n"+"Original error: %v", runID, runOutputDir, err)
}
err is still formatted with %v (line 324), not %w, so the returned error breaks the chain. This matters concretely here: the same file already relies on errors.Is(err, ErrNoArtifacts) at lines 281 and 283 to drive control flow for the other branch of this same recovery path, so callers of cacheRecoveryError's two call sites (lines 267-269, 296) reasonably expect the same chain-inspectable behavior and won't get it.
Why the linters still can't catch it
Both pkg/linters/errorfwrapv/errorfwrapv.go:70-73 and pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go:45-48 extract the format string identically:
cacheRecoveryError's format string is message+"...", a *ast.BinaryExpr (string concatenation), not a *ast.BasicLit, so both analyzers bail out before inspecting verbs or arguments. A repo-wide grep for concatenated fmt.Errorf(ident+"..." / fmt.Errorf("..."+ calls under pkg/ (non-test) turns up 6 production call sites; cacheRecoveryError is confirmed as the only one that both (a) uses a concatenated, non-literal format string and (b) trails a %v-formatted error argument - the other 5 already correctly use %w inside a concatenated literal, so today they're just invisible to the linter rather than actively wrong (a second-order enforcement-readiness gap, not a live bug).
Impact
Real bug, unresolved for the second review cycle: cacheRecoveryError's wrapped error loses chain identity; any future caller of the permission/cache-recovery path that tries errors.Is/errors.As on its result will silently fail to match, exactly mirroring the pattern the surrounding code already depends on.
Linter class gap spans two analyzers, not one: both errorfwrapv and fmterrorfnoverbs share the identical Args[0].(*ast.BasicLit) extraction, so a shared fix belongs in one place rather than being patched per-file, or the next concatenated-format-string call added anywhere in pkg/ will hit the same blind spot again in both directions (missed %w violations AND missed "no verbs, use errors.New" opportunities).
Recommendation
Fix cacheRecoveryError to use %w for the trailing err argument (single-line change, message rendering unaffected).
Harden format-string extraction to also resolve constant-foldable string concatenation (walk *ast.BinaryExpr chains of token.ADD over string literals/identifiers) - ideally as one shared helper in pkg/linters/internal/astutil that both errorfwrapv and fmterrorfnoverbs call, since they need the identical resolution logic.
Re-run the enforcement audit for both linters after the fix lands, before adding either to cgo.ymlLINTER_FLAGS.
Validation checklist
cacheRecoveryError wraps err with %w
New shared helper (or per-linter fix) resolves concatenated format strings in both errorfwrapv and fmterrorfnoverbs
New testdata case per linter covering a message + "literal"-style format string with a trailing error argument / no-verb string
pkg/cli/audit_run_pipeline_test.go gains a case asserting errors.Is/errors.As works through cacheRecoveryError's result
go test ./pkg/linters/... ./pkg/cli/... passes
Effort
Small-to-medium: a one-line %w fix in pkg/cli, plus a shared format-string-concatenation resolver used by two analyzer files, with matching testdata for both.
Filed by Sergo (automated Go static-analysis review) - re-filed after #56533 auto-expired unresolved.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
Overview
This exact bug was filed as #56533 by a prior Sergo run and auto-expired unfixed on 2026-09-03 (
state_reason: not_planned) - the self-expiry checkbox lapsed with zero code changes. Re-verified today (2026-09-05): both the production bug and the linter blind spot that hides it are still present, unchanged.Confirmed still-broken production code
pkg/cli/audit_run_pipeline.go:317-325:erris still formatted with%v(line 324), not%w, so the returned error breaks the chain. This matters concretely here: the same file already relies onerrors.Is(err, ErrNoArtifacts)at lines 281 and 283 to drive control flow for the other branch of this same recovery path, so callers ofcacheRecoveryError's two call sites (lines 267-269, 296) reasonably expect the same chain-inspectable behavior and won't get it.Why the linters still can't catch it
Both
pkg/linters/errorfwrapv/errorfwrapv.go:70-73andpkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go:45-48extract the format string identically:cacheRecoveryError's format string ismessage+"...", a*ast.BinaryExpr(string concatenation), not a*ast.BasicLit, so both analyzers bail out before inspecting verbs or arguments. A repo-wide grep for concatenatedfmt.Errorf(ident+"..."/fmt.Errorf("..."+calls underpkg/(non-test) turns up 6 production call sites;cacheRecoveryErroris confirmed as the only one that both (a) uses a concatenated, non-literal format string and (b) trails a%v-formatted error argument - the other 5 already correctly use%winside a concatenated literal, so today they're just invisible to the linter rather than actively wrong (a second-order enforcement-readiness gap, not a live bug).Impact
cacheRecoveryError's wrapped error loses chain identity; any future caller of the permission/cache-recovery path that trieserrors.Is/errors.Ason its result will silently fail to match, exactly mirroring the pattern the surrounding code already depends on.errorfwrapvandfmterrorfnoverbsshare the identicalArgs[0].(*ast.BasicLit)extraction, so a shared fix belongs in one place rather than being patched per-file, or the next concatenated-format-string call added anywhere inpkg/will hit the same blind spot again in both directions (missed%wviolations AND missed "no verbs, use errors.New" opportunities).Recommendation
cacheRecoveryErrorto use%wfor the trailingerrargument (single-line change, message rendering unaffected).*ast.BinaryExprchains oftoken.ADDover string literals/identifiers) - ideally as one shared helper inpkg/linters/internal/astutilthat botherrorfwrapvandfmterrorfnoverbscall, since they need the identical resolution logic.cgo.ymlLINTER_FLAGS.Validation checklist
cacheRecoveryErrorwrapserrwith%werrorfwrapvandfmterrorfnoverbsmessage + "literal"-style format string with a trailing error argument / no-verb stringpkg/cli/audit_run_pipeline_test.gogains a case assertingerrors.Is/errors.Asworks throughcacheRecoveryError's resultgo test ./pkg/linters/... ./pkg/cli/...passesEffort
Small-to-medium: a one-line
%wfix inpkg/cli, plus a shared format-string-concatenation resolver used by two analyzer files, with matching testdata for both.Filed by Sergo (automated Go static-analysis review) - re-filed after #56533 auto-expired unresolved.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.