See what it catches.
Run gox check ./... on a Go package. Every rule defaults to error and points at the exact line.
$ gox check ./... internal/auth/login.go:42:6: errcheck: error return from db.Close() dropped hint: assign to _ =, return it, or annotate with // safe-ignore: <why> internal/orders/transfer.go:118:5: namedargs: call to transfer(orderID, userID) needs labels hint: transfer(/* orderID */ a, /* userID */ b) pkg/parser/lex.go:201:8: shadow: declaration of "err" shadows outer variable cmd/worker/main.go:67:2: goroutine: fire-and-forget go run() without errgroup / WaitGroup / cancel internal/probe/health.go:14:6: httptimeout: http.Get uses http.DefaultClient which has no timeout hint: build an explicit *http.Client with a Timeout, or use NewRequestWithContext gox: 5 issue(s)
What it catches.
Each rule targets a high-frequency LLM bug class — silent failures that compile clean and pass tests.
error return values dropped silently.:= re-declaring an outer variable (except ok).x := v.(T) without the comma-ok form./* paramName */ labels.var declarations.any / interface{} without written justification.*http.Response.Body left unclosed.context.Background() / TODO() inside a function that already has a context.Context.go f() with no visible errgroup, WaitGroup, or CancelFunc.errors.Is / errors.As / %w instead of == / type-assert / %s on errors.Timeout.namedargs. Two adjacent strings or ints at a user-defined call site force the caller to label them inline. It prevents transfer(orderID, userID) vs transfer(userID, orderID) — no compile error, no test failure, and the single most common silent-bug class in unsupervised AI-written Go.
Every rule has one annotation.
Suppressions require a reason after the colon. Empty reasons are ignored — the reason is the documentation.
| Comment | Effect |
|---|---|
| // safe-ignore: <why> | Suppress errcheck, forcetypeassert, bodyclose, contextcheck on the same line. |
| // global-ok: <why> | Allow a package-level var (noglobals). |
| // any-ok: <why> | Allow any / interface{} (banany). |
| // goroutine-ok: <why> | Allow a fire-and-forget go statement (goroutine). |
| // exhaustive-ok: <why> | Accept default: as covering missing variants (exhaustive). |
| // timeout-ok: <why> | Allow an HTTP call or client constructed without an explicit Timeout (httptimeout). |
One go install away.
$ go install github.com/mentasystems/gox/cmd/gox@latest # analyze; exit 1 on any issue $ gox check ./... # list registered analyzers $ gox list # explain a single rule (markdown to stdout, or --json envelope) $ gox explain bodyclose # gox check && go build / go test $ gox build ./... $ gox test ./...
Drop gox into a legacy codebase without 2000 errors on day 1.
gox baseline captures the current state of issues into a .gox-baseline.json at the module root. From then on gox check reports only new violations — existing surface stays silent until someone touches it. Commit the file so the rest of the team gets the same view.
$ gox baseline captured 1742 issue(s) into .gox-baseline.json from now on, `gox check` will report only NEW issues. $ gox check ./... gox: 0 issue(s) # ... a new violation lands in a new commit ... $ gox check ./... internal/orders/new_handler.go:54:2: errcheck: error return from tx.Commit() dropped gox: 1 issue(s)
Wire it into your agent in one command.
gox install claude drops a Stop hook into ~/.claude/settings.json. When Claude finishes a turn, gox runs once on every package whose .go files changed. If anything fails, the hook returns a decision:block JSON blob with the full output — Claude sees it on the next turn and has to fix or annotate before continuing.
$ gox install claude ✓ wrote ~/.claude/gox-hook.sh ✓ registered Stop hook (30s) # idempotent — re-run to refresh the script.
settings.json when the /hooks menu is opened or the app restarts — open /hooks once and the hook activates in the current session. Earlier versions used a PostToolUse hook that fired on every edit; the Stop hook runs once per turn and is noticeably faster on large packages.
A tool, not a new language.
"Agent-native languages" are a tempting bet — reinvent the surface so the agent never has room to be wrong. The trade-off is heavy: new syntax the models don't know, a stdlib that has to be rebuilt, and design churn while the contract settles. gox takes the opposite path: leave Go alone and gate the silent bug classes at the door.
| gox | a fresh agent language | |
|---|---|---|
| Ecosystem | every Go library, day 1 | rebuild it |
| Model fluency | frontier models already write Go | teach them, line by line |
| Stability | Go 1.x compatibility promise | v0.x while the language settles |
| Adoption | go install, opt-in per repo | port the codebase |
| Cost of being wrong | uninstall it | rewrite back |
Pure Go. No external linters.
A pure-Go implementation on top of go/ast, go/types, and go list -json. Per-package cache keyed by file mtime+size and the analyzer-set hash.
Cache lives under $XDG_CACHE_HOME/gox/v2 (or ~/.cache/gox/v2). Pass --no-cache to disable.
Three principles.
go list -json. No x/tools, no third-party linter packages. When a new Go release lands, there's nothing to update.shadow exempts ok but flags err re-declaration, which is exactly the bug you want.