Open Source Security

go mod tidy: The Security Implications

Running go mod tidy feels like harmless housekeeping, but the command can silently pull new code, update checksums, and reshape your dependency graph in ways that have real security consequences.

Shadab Khan
Security Engineer
7 min read

Most Go developers type go mod tidy without thinking twice. It is the command you run when the compiler complains about missing modules, when the linter flags unused imports, or when CI rejects a commit because go.sum and go.mod have drifted. It feels like housekeeping. And it is, most of the time. But go mod tidy is also a command that reaches out to the proxy, downloads code, updates cryptographic checksums, and can quietly reshape your dependency graph. When I started looking at how teams actually use it, I realized that a surprising number of supply chain issues begin or end with an unreviewed tidy.

This post is a field guide to what go mod tidy does under the hood and where the sharp edges are. I am not going to tell you to stop running it. That would be absurd. But I will argue that the diff it produces deserves the same scrutiny as any other code change, and that your tooling should make that scrutiny cheap.

What tidy actually does

On the surface, go mod tidy has a simple job: make go.mod match the imports in your source tree, and make go.sum match the modules listed in go.mod. It adds missing entries and removes unused ones. Since Go 1.17 the default module graph was pruned, which means tidy also writes out checksum entries for the transitive dependencies your module actually needs at build time, not the full closure. Go 1.21 added the -compat flag behavior that keeps requirements compatible with older Go versions. Go 1.22, released on 6 February 2024, tightened how tidy handles the toolchain directive and made it more aggressive about recording minimum versions.

What most people miss is that tidy will, by default, consult the module proxy to fetch the latest minor or patch versions that satisfy your import graph. If a transitive dependency has a newer compatible release, tidy may select it. The checksums in go.sum then change. The code you build tomorrow is not the code you built yesterday, even though your source did not change.

Why is the go.sum diff worth reading?

The go.sum file is a trust boundary. Every line is a cryptographic commitment that a specific module version has a specific content hash. When go.sum changes, you are telling the build system: I accept these new hashes as authoritative. If you do not read the diff, you are trusting whoever produced the commit, whoever produced the module, and whoever controls the proxy that served it.

A real example worth remembering: in March 2023, researchers documented GO-2022-1059, a vulnerability in golang.org/x/text that affected versions before v0.3.8. Teams that ran go mod tidy without pinning policies in 2022 silently picked up the fixed version, which was the good outcome. But the opposite can happen too. A compromised or malicious minor release can slip in through a tidy that was run to fix an unrelated issue. CVE-2021-38561, in the same x/text package, is a reminder that widely-used modules are not immune.

I now treat go.sum diffs as code review material. If a pull request changes twenty lines of Go and eighty lines of go.sum, I want to know why. The PR author should be able to answer.

The proxy and the checksum database

When tidy runs, it talks to whatever is configured in GOPROXY. The default is https://proxy.golang.org,direct. It also consults the checksum database at sum.golang.org unless GOSUMDB is set to off or overridden. This two-layer verification is one of the best things about the Go ecosystem. The proxy serves immutable module content, and the checksum database publishes a transparent log of hashes that third parties can audit.

But defaults matter. If a developer on your team sets GOFLAGS=-insecure or runs GONOSUMCHECK=1 to work around a temporary proxy outage, and then runs tidy, you can end up with go.sum entries that were never verified against the public log. Go 1.21 removed some of the older insecure flags, but the environment is still a place where mistakes compound.

What about indirect dependencies?

Before the module graph pruning introduced in Go 1.17, go.mod listed every transitive module. After pruning, only the modules your code directly imports, plus the ones needed to satisfy the minimum version selection, are written. This is cleaner, but it also means that tidy can add or remove // indirect lines in ways that are not obvious at a glance.

I have seen teams that set up CI to fail if go mod tidy would change anything, which is a good baseline. The Go team itself uses the -diff flag, added in Go 1.23 (released 13 August 2024), which prints what would change without modifying files. In CI, go mod tidy -diff combined with git diff --exit-code gives you a clean gate.

Tidy and replaced modules

replace directives complicate the picture. If your go.mod contains a replace pointing at a local path or a fork, tidy will respect it, but it will not verify the replacement against sum.golang.org. The content of the replaced module is trusted based on whatever the local filesystem or the forked repository contains. This is by design, but it means that anyone who can write to the replacement source can poison your build. In a monorepo with many replace directives, this surface grows quickly.

How do you make tidy safer in practice?

A few habits have served me well. First, run tidy explicitly as its own commit, never bundled with feature code. The diff is easier to review when it stands alone. Second, adopt a policy in CI that fails if go mod tidy -diff produces output, so that drift cannot accumulate silently. Third, pin your Go toolchain version in go.mod using the toolchain directive, because different Go versions produce different tidy results, especially across the 1.21 and 1.22 boundary. Fourth, audit the go.sum diff in every PR, even if it is long. If you cannot explain why a hash changed, do not merge.

Tooling helps. govulncheck, which went GA in September 2022 and reached v1.0.0 on 13 July 2023, can be run against the resolved module graph after tidy to surface known vulnerabilities that were pulled in. dependabot and renovate will open PRs for version bumps that your team can review deliberately, rather than having tidy do it as a side effect.

The cultural problem

The technical controls only work if the team treats them seriously. I have worked with engineers who see a failing tidy check in CI and immediately run go mod tidy locally, commit the result, and push without looking at the diff. That is the exact failure mode the gate was built to prevent. Training, code review standards, and a blameless culture around supply chain hygiene matter more than any single tool.

How Safeguard Helps

Safeguard watches the module graph produced by go mod tidy and flags changes that matter. When go.sum updates during a build, Safeguard correlates the new hashes against our vulnerability and provenance database and surfaces any module that moved into a risky version band, was recently yanked, or has a maintainer change that looks suspicious. Policy gates can block merges that pull in unreviewed indirect dependencies or that modify replace directives without approval. For teams that run govulncheck in CI, Safeguard aggregates the results across repositories so you see supply chain drift at the organization level rather than one PR at a time.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.