When to use replace in go.mod, and when not to.
Owner: go-quality-assistant
Applies when: a Go project's go.mod contains a replace directive whose right-hand side path points outside the current repo's working tree — relative paths that escape the repo root (../../other-repo/lib), absolute filesystem paths (/Users/<name>/work/...), or any other off-repo location.
Enforcement: scripts/rule-checks.sh (greps go.mod for replace directives with absolute paths or ../ paths that resolve outside the repo root; same-repo relative replaces are exempt)
Why: An off-repo replace points to a path that exists only on the author's machine. Anyone else cloning the repo gets a broken build; CI can't resolve it; the module graph becomes non-reproducible. Worse, it hides the fact that consumers of your module require an unreleased change — the dependency looks fine locally but breaks the moment someone else pulls it. Within a monorepo, relative-path replaces ARE correct (every clone has the sibling module at the same path) — that's the same-repo exception. The rule fires only on the cross-repo escape.
// consumer/go.mod
replace github.com/acme/producer/lib => ../../producer/lib
// Builds locally, breaks for everyone else. CI fails: path doesn't exist in their checkout.
// Or worse: absolute path tied to one machine
replace github.com/acme/producer/lib => /Users/alice/work/producer/lib// consumer/go.mod — pin to a tagged or pseudo-version
require github.com/acme/producer/lib v0.0.0-20260403114524-913de8870914
// No replace. Reproducible for everyone.
// monorepo/services/api/go.mod — same-repo replace IS correct
replace github.com/acme/monorepo/libs/shared => ../../libs/shared- Replace inside a single repo → OK. Multi-module repos (monorepos with several
go.mod) use relative-path replaces so sibling modules resolve to the working tree, not a released version. - Replace across repo boundaries → NO. Never use a
replacethat points outside the current repo's checkout. Consume cross-repo modules as released pseudo-versions or tagged releases.
Inside repo: the replace is part of the monorepo's contract. Every clone has the sibling module at the same relative path. CI, builds, and contributors see the same resolution. Changes to the sibling are reviewed in the same PR.
Across repos: the replace points to a path that only exists on your machine. Anyone else cloning the repo gets a broken build. CI can't resolve it. The module graph becomes non-reproducible. Worse, it hides the fact that consumers of your module require an unreleased change — the dependency looks fine locally but breaks the moment someone else pulls it.
// services/api/go.mod
module github.com/acme/monorepo/services/api
require github.com/acme/monorepo/libs/shared v0.0.0-...
replace github.com/acme/monorepo/libs/shared => ../../libs/sharedSibling module libs/shared is always available at the relative path; the replace pins local development to the working tree.
// consumer/lib/go.mod
module github.com/acme/consumer/lib
require github.com/acme/producer/lib v0.0.0-20260403114524-913de8870914
// NO replace directive — module resolves via GOPROXYThe consumer pulls a specific tagged or pseudo-version from the module proxy. To upgrade, bump the version string and run go mod tidy.
- Make change in producer repo (e.g.
acme/producer). - Commit and push.
- Tag or let the commit be pseudo-versioned.
- In consumer repo (e.g.
acme/consumer), rungo get github.com/acme/producer/lib@<version>andgo mod tidy.
Never shortcut step 2-3 with a local replace. The extra ceremony exists specifically to prevent broken builds for everyone else.
None in practice. If you feel you need a cross-repo replace, one of these is almost always better:
- Publish a pre-release version (
v0.0.0-YYYYMMDDHHMMSS-commitsha) and pin to it. - Vendor the producer code temporarily.
- Restructure so the two modules live in the same repo.
// ❌ BAD — cross-repo relative replace
// consumer/go.mod
replace github.com/acme/producer/lib => ../../producer/lib
// Builds locally, breaks for everyone else. CI fails: path doesn't exist in their checkout.// ❌ BAD — replace to "test" an unreleased change
replace github.com/acme/producer/lib => /Users/alice/work/producer/lib
// Machine-specific absolute path. Hides the fact the consumer requires unreleased code.// ✅ GOOD — pseudo-version after producer push
require github.com/acme/producer/lib v0.0.0-20260403114524-913de8870914
// No replace. Reproducible for everyone.- Go modules docs: replace directive
- Go Mod Dependency Fix — troubleshooting module resolution