Writing an AGENTS.md for Go
Go needs a shorter instructions file than any other language, because the compiler already enforces most of what you would otherwise write down.
AGENTS.md is a Markdown file in your repository root that coding agents read before they start. Claude Code reads CLAUDE.md; Codex, Cursor, Aider and most newer tools read AGENTS.md. Same contents, so write one and symlink:
ln -s AGENTS.md CLAUDE.mdThe file is not documentation. It is a standing correction list: the things a competent Go developer would get wrong in their first week in your repo.
Go's file should be the shortest one you write#
Every token here is prepended to every request for the whole session. It is a permanent tax on context, and — more importantly — a long list dilutes the rules you care about. Twelve rules get followed; sixty get sampled.
Go has an unusual advantage: gofmt, go vet, staticcheck and the compiler already enforce most of what other languages need prose for. Do not write down what golangci-lint will tell the model in one second.
The target
Under 60 lines. If it is longer, most of it is either already enforced by a tool or is documentation that belongs in docs/ behind a one-line pointer.
What not to write#
Delete these on sight. Every one is already covered:
| Do not write | Because |
|---|---|
| "Format your code" | gofmt. There is no choice to make. |
| "Remove unused imports and variables" | Compile errors. It cannot ship. |
| "Use camelCase, exported names are capitalised" | The language enforces visibility; staticcheck covers naming. |
| "Handle errors" | Ignoring one requires writing _, which errcheck flags. |
| "Add comments to exported functions" | revive / staticcheck rule. Configure it once. |
| "Don't use deprecated stdlib" | staticcheck SA1019. |
That last one is worth a caveat: staticcheck catches deprecations, but models still reach for ioutil, interface{} and hand-rolled contains because the training data is full of them. One line covering the whole class is worth it — see the template.
What is worth writing#
How to run things. Unguessable and used every turn.
Concurrency policy. The one area where the compiler stops helping and where generated Go is genuinely weak. This is the highest-value section in a Go AGENTS.md.
Interface placement. Consumer-side interfaces are idiomatic Go and models trained on a lot of Java-shaped Go will not do it by default.
Landmines. What breaks if changed. Almost nobody writes these and they are worth more than everything else combined.
The template#
Go 1.23. Layout: cmd/ (binaries), internal/ (everything real), pkg/ (only if
genuinely importable by other repos).
## Commands
- Everything: `make check` (gofmt, go vet, golangci-lint, go test -race)
- Test: `go test -race -count=1 -timeout 60s ./...`
- One test: `go test -run TestName ./internal/pkg -v`
- Bench: `go test -bench=. -benchmem ./internal/pkg`
`-race` is always on and `-count=1` disables caching. Do not remove either to
make the suite faster.
## Errors
- Wrap with context and %w: `fmt.Errorf("load user %s: %w", id, err)`.
Never %v — it silently breaks errors.Is for every caller.
- Compare with errors.Is / errors.As, never ==.
- Handle once: add context and return. Do not log and return the same error.
- No naked returns. No panic outside main() and package init.
## Concurrency
- Every goroutine needs a guaranteed exit path. If it can block on a send,
buffer the channel or give it a context.
- Prefer errgroup.WithContext over WaitGroup + channels by hand.
- context.Context is the first parameter of anything doing I/O, and is
actually plumbed through — not accepted and dropped.
- TestMain calls goleak.VerifyTestMain.
- Never range a map to produce output. Sort the keys.
## Style the linter cannot enforce
- Interfaces are declared by the CONSUMER, in the consumer's package, and are
small. One or two methods. Return concrete types.
- Table-driven tests with t.Run subtests.
- Use the current stdlib: os.ReadFile not ioutil, any not interface{},
slices/maps packages, log/slog not logrus, math/rand/v2.
- No dependency injection framework. Wire it explicitly in main().
## Landmines
- internal/scheduler is leader-elected. Changing tick timing needs an ops review.
- internal/proto is generated. Edit the .proto and run `make proto`.
- cmd/migrate: write migrations, never run them. A human runs migrations.Fifty lines, and almost none of it is generic Go advice — it is facts about this repository plus the two areas (errors, concurrency) where tooling alone is not enough.
Push the enforceable parts into config#
Anything you can enforce, enforce — a check that fails beats a sentence that competes for attention.
linters:
enable:
- errcheck # ignored errors
- errorlint # %w and errors.Is — catches the wrapping rule above
- contextcheck # dropped contexts
- govet # lostcancel, copylocks, printf
- staticcheck # deprecations, and most of the rest
- bodyclose
- noctx
- gosecThat config removes three lines from the file above and makes them non-negotiable instead of advisory. Same trade every time it is available.
Nested files in a monorepo#
Most tools read the nearest AGENTS.md and merge upward. In a repo with several services, keep the root file to commands and layout and push specifics down:
AGENTS.md commands, layout, error and concurrency policy
internal/billing/AGENTS.md money is int64 cents; the Stripe webhook landmine
internal/ingest/AGENTS.md backpressure rules, the bounded worker poolIs yours working?#
Run the same non-trivial task twice — once with the file, once with it renamed away. If the diffs differ meaningfully, it is earning its place. If they do not, you have written fifty lines of ballast.
It is also worth deleting a third of it every few months and seeing whether anything gets worse. In Go, usually nothing does, because the toolchain was already covering it.
Common questions#
Why is the Go file shorter than the Python one?#
Because more of the equivalent content is enforced mechanically. gofmt removes every style rule, the compiler removes every dead-code rule, and errcheck removes the error-handling rule. What is left is genuinely repo-specific — which is what the file should have been all along.
Should I mention the Go 1.22 loop variable change?#
Only if you see it causing confusion. Models trained on older code sometimes emit the x := x shadowing workaround, which is now unnecessary but harmless. The one worth catching is generated code that relies on the old sharing behaviour, and that is a bug a reviewer should spot rather than a line in a file.
Do I need a section on generics?#
A line, if your codebase uses them: "prefer concrete types; generics need a reason." Generated generic Go tends to be more elaborate than the problem requires, because there is comparatively little good generic Go in the training data.
Get the Go agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.