# Writing an AGENTS.md for Go

> Source: https://learn-go.org/ai/agents-md/
> Part of Learn Go, free to read.

`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:

```bash
ln -s AGENTS.md CLAUDE.md
```

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

:::verdict 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](/review/failure-modes/). 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

```markdown AGENTS.md
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.

:::tip The two-strike rule
Do not add a rule speculatively. Wait until an agent has made the same mistake twice. It keeps the file short, keeps every line evidence-backed, and tells you which of your conventions are genuinely non-obvious.
:::

## Push the enforceable parts into config

Anything you *can* enforce, enforce — a check that fails beats a sentence that competes for attention.

```yaml .golangci.yml
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
    - gosec
```

That 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:

```text
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 pool
```

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