Review & Verify Updated 2026-09 8 min read View as Markdown

Dependency hygiene for Go modules

Go's module system already solves most of what goes wrong elsewhere. What is left is hallucinated import paths, and the handful of directives that quietly disable the guarantees.

Go has the best default supply-chain story of any language on this network, and it is worth knowing why before looking at what can still go wrong.

  • go.sum is mandatory and checked. Every module version is pinned by hash and verified on every build.
  • The checksum database cross-checks those hashes against a public transparency log, so a module author cannot quietly change a published version.
  • Minimal version selection means you get the lowest version that satisfies all requirements, not the newest. Builds do not change under you because someone published a release.
  • govulncheck does reachability analysis, reporting only vulnerabilities your code can actually reach.

That removes most of the attack surface that dominates npm and PyPI. What is left is smaller and more specific.

Hallucinated import paths#

Models invent module paths, and Go's are compositional enough to invent convincingly:

go
import (
    "github.com/gorilla/websocket"        // real
    "github.com/gin-gonic/gin/middleware" // plausible subpackage, does not exist
    "github.com/uber-go/zap"              // wrong path: it is go.uber.org/zap
    "golang.org/x/exp/maps"               // moved into the stdlib; may not be what you want
)

Three flavours worth recognising:

  • Wrong vanity path. go.uber.org/zap, not github.com/uber-go/zap. Both look right.
  • Invented subpackage. The module exists; that path within it does not.
  • Stale location. A package that moved from golang.org/x/exp into the standard library, or between major versions.

The good news: go build fails immediately and cheaply. Unlike npm or pip, nothing gets installed and no code executes during resolution — Go modules have no install scripts, which removes the entire slopsquatting execution vector.

The bad news: someone can still register a plausible module path and wait. The check is the same as everywhere — before adding a dependency you did not choose deliberately, look at the repository: does it have history, other users, and issues from real people?

shell
go list -m -json github.com/some/module@latest   # origin, version, checksum

The directives that disable the guarantees#

Three things in go.mod and the environment quietly turn off the protections above. All three have legitimate uses; all three deserve a comment.

replace#

go
replace github.com/upstream/lib => ../local-fork
replace github.com/upstream/lib => github.com/ourfork/lib v1.2.3

A local replace pointing outside the repository makes your build non-reproducible for anyone else. A replace to a fork is fine and should say why, with a link to the upstream issue and a plan to remove it.

Catch it with: grep -n '^replace' go.mod in review. Note that replace directives in a dependency's go.mod are ignored — they only apply in the main module — which surprises people.

GOFLAGS=-mod=mod and GONOSUMDB/GOPRIVATE/GONOSUMCHECK#

shell
GOPRIVATE=github.com/yourorg/*     # skips proxy AND checksum db for these paths
GONOSUMDB=*                        # skips checksum verification. never do this.

GOPRIVATE is correct and necessary for internal modules. A wildcard that disables checksum verification broadly is not — and it is a common "fix" for a confusing build error.

// indirect accumulation#

go mod tidy keeps this honest. A go.mod that has not been tidied accumulates requirements nothing uses, which inflates your vulnerability surface for no benefit.

shell
go mod tidy && git diff --exit-code go.mod go.sum   # in CI: fails if not tidy

Reduce the surface#

Go's standard library is large, and generated Go reaches for dependencies it does not need — partly because a lot of pre-2021 Go in the training data predates the stdlib additions.

Generated reaches forOften unnecessary because
logrus, zaplog/slog is stdlib since 1.21
github.com/pkg/errorsfmt.Errorf with %w, errors.Is/As
gorilla/mux, chi for simple routingnet/http has method and wildcard patterns since 1.22
a min/max/contains helperbuiltins and slices
github.com/google/uuid for a random idcrypto/rand plus encoding, if you do not need UUID format
testifythe stdlib testing package plus cmp.Diff
godotenvread the file, or use your platform's config

testify is worth singling out because it is nearly universal and genuinely optional: table-driven tests with cmp.Diff for comparison cover almost everything, with one fewer dependency and no assertion DSL to learn.

Put the rule in your AGENTS.md: new dependencies need a sentence justifying them; prefer the standard library.

Read the go.sum diff#

shell
go get github.com/some/lib
git diff go.mod go.sum

A one-line go.mod change is often a forty-line go.sum change. That diff is the transitive dependencies you just accepted, and it is the only place you see them. A dependency that pulls in twenty modules for one function is a decision, not an accident.

shell
go mod graph | wc -l                        # how big is the graph?
go mod why github.com/surprising/module     # why is this here at all?

go mod why is the tool for the "where did that come from" moment, and it gives you the actual import chain.

Keep it current#

shell
govulncheck ./...                           # in CI, on every build
go list -m -u all                           # what has newer versions
go get -u ./... && go mod tidy && go test ./...

govulncheck in CI is the highest-value item here. Because it does reachability analysis, a failure means something real, so people fix it rather than muting it — which is the failure mode of noisier scanners in other ecosystems.

Enable Dependabot or Renovate as well. Not because each update matters individually, but because a repository where updates arrive continuously is one where a security update can be merged in an afternoon rather than being a project.

Vendoring#

shell
go mod vendor        # commits the dependency source into vendor/

Worth it when you need builds that work with no network, or an auditable snapshot of exactly what ships. The cost is a large diff on every update and a vendor/ directory people scroll past in review. For most projects, go.sum plus a module proxy gives you the same guarantees with less noise.

The whole policy

  1. go mod tidy clean, enforced in CI.
  2. govulncheck in CI. Act on it; it is low-noise by design.
  3. Read the go.sum diff when adding anything.
  4. Every replace has a comment saying why and when it goes.
  5. Never disable checksum verification to make an error go away.

Common questions#

Can a Go module run code at install time?#

No. There is no install or post-install hook — go get downloads and verifies source, and nothing executes until you build and run. That removes the single most dangerous property of npm and PyPI installs, and it is why package-install permission matters less in Go than elsewhere. Build-time code generation via go:generate is explicit and only runs when you ask.

Should package installs still go behind a confirmation for my agent?#

Less critical than in other ecosystems, but still worth it — not for the execution risk, which is absent, but because it makes you read the module path before it enters go.mod. Two seconds, and it catches the wrong-vanity-path case.

What does minimal version selection mean in practice?#

You get the lowest version that satisfies every requirement in the graph, so adding a dependency does not silently upgrade your others, and builds are reproducible without a lockfile ceremony. Upgrades are explicit acts via go get, which is the behaviour you want.

Is a private module proxy worth running?#

For a team with internal modules, yes — it gives you availability independent of upstream, a cache, and a single place to enforce policy. For a solo project, GOPRIVATE pointed at your VCS is enough.

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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.