gox

module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 14, 2026 License: BSD-3-Clause

README

gox

ci release license

Strict static analyzer for Go. Zero external dependencies — every rule is implemented from scratch on top of go/ast, go/types, and the go list command.

demo

The bug above (transfer("o-42", "u-7") with parameters declared as userID, orderID string) compiles, passes every test, and ships to production. No major Go linter today catches this class. gox does, by demanding inline /* paramName */ comments at the call site whenever two adjacent arguments share a type.

The goal: catch the classes of bugs an LLM writing Go without supervision is most likely to introduce. Be loud, be opinionated, fail closed.

Status: experimental. Built as a tool for using Claude Code (and similar agents) to write Go without leaving silent bugs behind. Used daily, but the rule set is still evolving — expect breaking changes to annotation syntax until v1.0.

Install

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

Use

gox check ./...    # analyze; exit 1 on any issue
gox list           # list registered analyzers
gox build [args]   # gox check && go build
gox test  [args]   # gox check && go test

Rules

Analyzer What it catches
errcheck error return values dropped silently
shadow := re-declaring an outer variable (except ok)
forcetypeassert x := v.(T) without the comma-ok form
namedargs call sites passing 2+ args of the same basic type without /* paramName */ comments (user code only — stdlib calls exempt)
exhaustive non-exhaustive switch on iota enums or sealed interfaces
noglobals mutable package-level var declarations
banany any / interface{} in declarations without justification
bodyclose *http.Response.Body left unclosed
contextcheck context.Background()/TODO() inside a function that already receives a context.Context
goroutine go f() without a visible *errgroup.Group, sync.WaitGroup, or context.CancelFunc

Annotations

Every rule has a single opt-out marker. Annotations must include a reason after the colon — empty reasons are ignored.

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 a default: case as covering missing variants (exhaustive)

Claude Code integration

gox install claude

Writes ~/.claude/gox-hook.sh and registers it as a Stop hook in ~/.claude/settings.json (timeout 30s). Idempotent — running it again just refreshes the script. Preserves every other key in settings.json. If an older gox install registered a PostToolUse hook, this command migrates it away automatically.

When Claude finishes a turn, the hook looks at the .go files changed in the current git repo (unstaged, staged, and untracked) and runs gox check once per affected package. If issues are found, the hook returns a decision:block JSON blob with the full output, which Claude sees on its next turn and must fix or annotate before continuing.

It runs once per turn rather than on every edit — earlier versions used a PostToolUse hook that fired on each Edit/Write, which was noticeably slow on large packages.

Claude Code only re-reads settings.json when the /hooks menu is opened or the app is restarted, so the hook activates in a new session (or after opening /hooks once in the current one).

The hook resolves gox via $GOX_BIN if set, otherwise ~/go/bin/gox. Run go install github.com/mentasystems/gox/cmd/gox@latest to make sure it's present.

Performance

A pure-Go implementation with no runtime overhead from external linters. On a 442-package monorepo (~1800 .go files):

Time
Cold run (no cache) ~9.4s
Warm run (full cache hit) ~2.6s

The cache is per-package, keyed by file mtime+size and the analyzer set hash. It lives under $XDG_CACHE_HOME/gox/v2 (or ~/.cache/gox/v2). Pass --no-cache to disable.

Output cap

gox check prints at most 100 issues by default, then a one-line summary of how many were hidden. This keeps the output bounded for context-limited consumers — notably the Claude Code Stop hook, which pipes gox check straight back into the model. Raise or lift the cap with --max-issues=N (0 = unlimited) or the GOX_MAX_ISSUES env var. Issues are sorted by file:line, so the truncation is deterministic.

Generated code

Files marked with the standard Go marker // Code generated <generator> DO NOT EDIT. near the top are skipped automatically. This covers protoc-gen-go, yo, mockgen, and most common generators.

How is this different from staticcheck / golangci-lint / revive?

Short answer: every existing Go linter is a detector. gox is a gate.

The existing tools surface warnings and let humans decide. That works when the human is the one writing the code. When an LLM is writing the code at the speed of a few tokens per second and no one is reviewing line-by-line, "surface a warning" is the wrong default — the warning will be ignored unless something downstream refuses to proceed.

Concrete differences:

gox golangci-lint / staticcheck / revive
Default severity every rule is an error most rules are warnings
Opt-out one annotation with a written reason on the same line regex/path config files
Dependencies none — only Go stdlib + go list hundreds of transitive deps via golang.org/x/tools
Audience code written by LLM agents (Claude Code, Cursor, etc.) humans, optionally CI
Coverage 11 rules, picked for high signal in unsupervised codegen hundreds of rules; you pick the subset
Ergonomics gox install claude wires it into the LLM's tool loop manual config + pre-commit / CI plumbing

The bug that motivated gox is the swap-prone call site:

transfer(orderID, userID)   // vs transfer(userID, orderID)

Both compile. Both pass tests. The wrong one ships and corrupts the ledger. No major linter catches this today; gox's namedargs rule forces inline /* paramName */ comments on adjacent same-type arguments at the call site, which:

  • catches the bug,
  • documents the call site,
  • and the LLM annotating its own code is essentially free.

That trade — "more typing at the call site, near-zero bugs of this class" — only makes sense when typing is cheap, which is now.

If you already love golangci-lint, keep it. gox is meant to live alongside it, not replace it: golangci-lint is the spell-checker, gox is the production gate.

Design notes

  • Zero external dependencies. Everything uses Go stdlib + a shell-out to go list -json. No golang.org/x/tools, no third-party linter packages. Minimal maintenance: when a new Go release ships, there's nothing to update.
  • Fail closed. Every rule defaults to error. Opt-outs require an explicit annotation with a written reason — the reason is the documentation.
  • Targeted at LLM-written code. Heuristics are tuned so each rule catches a high-frequency LLM bug class without flooding human-readable idioms. For example, shadow exempts ok (the universal comma-ok name) but still catches err re-declaration, which is exactly the bug we want.
  • namedargs is the killer rule. Two adjacent string/int/bool parameters in user-defined code force the call site to label them with inline comments. Stdlib calls are exempt because their conventions are memorized. The bug it prevents — transfer(orderID, userID) vs transfer(userID, orderID) — produces no compile error and no test failure, and is the single most common silent-bug class in unsupervised AI-written Go.

Contributing

Issues and pull requests welcome. Two ground rules:

  1. No external dependencies. Every rule must be implementable with the Go standard library plus an out-of-process go list -json call. Adding golang.org/x/tools/go/packages, staticcheck, or any third-party linter is out of scope.
  2. Each new rule must pass its own check. Run gox check ./... on a fresh clone before opening a PR — gox runs against its own source as part of the smoke test.

License

BSD 3-Clause. See LICENSE.

Directories

Path Synopsis
cmd
gox command
gox is a strict static analyzer for Go.
gox is a strict static analyzer for Go.
internal
astutil
Package astutil contains small helpers shared by multiple analyzers.
Package astutil contains small helpers shared by multiple analyzers.
pkg
analyzer
Package analyzer defines the core types every gox analyzer implements.
Package analyzer defines the core types every gox analyzer implements.
analyzer/analyzertest
Package analyzertest provides helpers for unit-testing individual analyzers without going through the loader / `go list` pipeline.
Package analyzertest provides helpers for unit-testing individual analyzers without going through the loader / `go list` pipeline.
analyzers/banany
Package banany forbids `any` / `interface{}` in declarations without an `// any-ok: <reason>` annotation on the same line (or on a doc comment).
Package banany forbids `any` / `interface{}` in declarations without an `// any-ok: <reason>` annotation on the same line (or on a doc comment).
analyzers/bodyclose
Package bodyclose reports HTTP responses whose Body is never closed.
Package bodyclose reports HTTP responses whose Body is never closed.
analyzers/contextcheck
Package contextcheck enforces context propagation.
Package contextcheck enforces context propagation.
analyzers/errcheck
Package errcheck reports calls to functions returning `error` whose error value is silently dropped.
Package errcheck reports calls to functions returning `error` whose error value is silently dropped.
analyzers/errorlint
Package errorlint reports incorrect handling of error values.
Package errorlint reports incorrect handling of error values.
analyzers/exhaustive
Package exhaustive enforces switch exhaustiveness over enums and sealed interfaces defined in the analyzed packages.
Package exhaustive enforces switch exhaustiveness over enums and sealed interfaces defined in the analyzed packages.
analyzers/forcetypeassert
Package forcetypeassert forbids type assertions that would panic on failure.
Package forcetypeassert forbids type assertions that would panic on failure.
analyzers/goroutine
Package goroutine reports `go` statements whose lifetime is not bound to a visible coordination primitive.
Package goroutine reports `go` statements whose lifetime is not bound to a visible coordination primitive.
analyzers/namedargs
Package namedargs requires call sites to name their arguments when two or more consecutive parameters share the same type.
Package namedargs requires call sites to name their arguments when two or more consecutive parameters share the same type.
analyzers/noglobals
Package noglobals forbids package-level mutable `var` declarations.
Package noglobals forbids package-level mutable `var` declarations.
analyzers/shadow
Package shadow reports variable shadowing introduced by `:=`.
Package shadow reports variable shadowing introduced by `:=`.
baseline
Package baseline implements the "ignore pre-existing issues" workflow.
Package baseline implements the "ignore pre-existing issues" workflow.
cache
Package cache provides a per-package incremental cache for gox analyzer results.
Package cache provides a per-package incremental cache for gox analyzer results.
loader
Package loader parses and type-checks Go packages using only the standard library.
Package loader parses and type-checks Go packages using only the standard library.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL