Experimental · used daily

gox

A strict, opinionated static analyzer for Go. Built to catch the silent bugs an LLM writing Go without supervision is most likely to introduce — be loud, be opinionated, fail closed.

See what it catches.

Run gox check ./... on a Go package. The default run is the bug tier — every finding defaults to error and points at the exact line. Add --all for a one-off pass with the opt-in style tier.

~/code/my-service
$ 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/cache/store.go:88:9: forcetypeassert: type assertion without comma-ok will panic on mismatch
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: 3 issue(s)

# one-off full run: adds the opt-in style tier
$ gox check --all ./...
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
gox: 5 issue(s)

What it catches.

Each rule targets a high-frequency LLM bug class — silent failures that compile clean and pass tests. The bug tier runs by default. The style tier is opt-in (--all): a month of real agent transcripts showed that, enforced in an agent loop, style rules mostly produce suppression annotations instead of fixes — noise that drowns the bug tier.

errcheck
error return values dropped silently.
forcetypeassert
x := v.(T) without the comma-ok form.
exhaustive
Non-exhaustive switch on iota enums or sealed interfaces.
bodyclose
*http.Response.Body left unclosed.
contextcheck
context.Background() / TODO() inside a function that already has a context.Context.
errorlint
errors.Is / errors.As / %w instead of == / type-assert / %s on errors.
httptimeout
HTTP clients and shortcut calls must set an explicit Timeout.
shadow
:= re-declaring an outer variable (except ok).
namedargs
2+ args of the same basic type at user-code call sites without /* paramName */ labels.
noglobals
Mutable package-level var declarations.
banany
any / interface{} without written justification.
goroutine
go f() with no visible errgroup, WaitGroup, or CancelFunc.
The boldest rule is 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. It lives in the opt-in style tier: enforce it in CI with --all rather than inside the agent loop.

Every rule has one annotation.

Suppressions require a reason after the colon. Empty reasons are ignored — the reason is the documentation.

CommentEffect
// 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.

shell
$ go install github.com/mentasystems/gox/cmd/gox@latest

# run the default (bug-tier) analyzers; exit 1 on any issue
$ gox check ./...

# one-off full run including the opt-in style tier
$ gox check --all ./...

# 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.

shell
$ 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 the bug tier once on every package whose .go files changed during that turn (a file left dirty in git doesn't re-block unrelated turns), and never repeats a byte-identical report. If anything fails, the hook returns a decision:block JSON blob — Claude sees it on the next turn and has to fix or annotate before continuing. Pass extra flags with GOX_HOOK_FLAGS (e.g. "--all").

shell
$ gox install claude
 wrote ~/.claude/gox-hook.sh
 registered Stop hook (30s)
# idempotent — re-run to refresh the script.
Claude Code only re-reads 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.

goxa fresh agent language
Ecosystemevery Go library, day 1rebuild it
Model fluencyfrontier models already write Goteach them, line by line
StabilityGo 1.x compatibility promisev0.x while the language settles
Adoptiongo install, opt-in per repoport the codebase
Cost of being wronguninstall itrewrite back
You don't need a new language to make agent-written Go safe. You need strict default-error rules at the exact bug classes that compile clean and pass tests. That's the whole job.

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.

~9.4s
Cold run · no cache
~2.6s
Warm run · full cache
442pkg
~1800 .go files

Cache lives under $XDG_CACHE_HOME/gox/v2 (or ~/.cache/gox/v2). Pass --no-cache to disable.

Three principles.

zero deps
Go stdlib + go list -json. No x/tools, no third-party linter packages. When a new Go release lands, there's nothing to update.
fail closed
Every enabled rule defaults to error. Opt-outs require an explicit annotation with a written reason. The reason is the doc.
tuned for LLMs
Heuristics catch high-frequency LLM bug classes without flooding human idioms — shadow exempts ok but flags err re-declaration, which is exactly the bug you want.