What does go.mod's `tool` directive do, and how does it stop tool-version drift between laptops and CI?
answer
- a build's helpers are dependencies too
- record it where versions already live
- one directive, one command to run it
- the older trick used blank imports
- it pins the tool, not the Go release
basics
~20 sA tool directive records a helper program's package path in go.mod, so its version resolves from the module graph like any dependency. Running go tool with that program's name builds and runs the pinned version, not whatever binary is on PATH.
solid answer
~50 sBefore Go 1.24, a repository's helper programs lived outside the module: each engineer installed a binary somewhere on `PATH`, and the version they got depended on when they installed it. That is the classic "green on my laptop, red in the pipeline" failure, because the check ran two different builds of the same program. The `tool` directive fixes it by putting the tool inside the module: `go get -tool example.com/cmd/thing` adds a `tool` line naming the package plus a `require` on the module that provides it, and `go tool thing` then builds and runs that resolved version. Every machine that checks out the repository gets the same source, and upgrading is a commit to `go.mod` that everyone sees in review. The tool's own dependencies join the main module's requirement graph, which is the tradeoff, and `go tool` on its own lists both the built-in toolchain programs and the module's.
code
mod · 5 linesmodule example.com/svc
go 1.24
tool golang.org/x/tools/cmd/goimportsgo deeper
Know that a helper program the build runs can be declared in go.mod and run with go tool, so everyone gets the same version rather than whatever binary they installed once.
Explain the mechanics: go get -tool writes the directive and the require, the version resolves through the module graph, go tool builds from that pinned source, and go mod tidy keeps it honest.
Diagnose drift with it. Show that pinning the tool does not pin the Go release, and that a check disagreeing between laptop and pipeline is usually one of those two versions, not the author's change.
Own the policy: which tools belong in the module graph at all, who reviews an upgrade that changes generated output, and when a shared heavyweight tool is better kept out of every module's requirements.
## The problem: tools that live outside the module A Go repository's build usually runs a few programs that are not the compiler: a code generator, a formatter variant, a schema or mock producer. Historically these were not part of the module at all. Each engineer obtained a binary and put it on `PATH`; CI obtained one in its setup step. Nothing recorded which build any of them was, so the same command produced different bytes on different machines. The symptom is not a compile error, it is a **check that is green on the laptop and red in the pipeline**: the formatting or generation step produces output the other environment did not produce, and the diff is blamed on the author who happened to push. The long-standing workaround was a `tools.go` file: a file excluded from normal builds by a build constraint, containing blank imports of the tool packages. Because they were imports, `go mod tidy` kept the corresponding modules in `go.mod` and their exact versions in `go.sum`, so the version was at least *recorded*. It was a trick — a file that exists to lie to the dependency graph — and it still did not give you a command that ran the pinned build. ## What the `tool` directive is Go 1.24 made it first class. `go.mod` gained a `tool` directive naming a **package path** whose package is a `main` package: ``` module example.com/svc go 1.24 tool golang.org/x/tools/cmd/goimports ``` You do not usually type it: `go get -tool golang.org/x/tools/cmd/goimports` adds the `tool` line and the matching `require` on the providing module, and records its hashes in `go.sum`. The version is then resolved exactly like every other dependency version in the module graph, with the same review trail — an upgrade is a diff in `go.mod`, not an undocumented change on somebody's machine. ## Running it `go tool <name>` runs it, where `<name>` is the last element of the package path. The go command builds the tool from the pinned source if it is not already in the build cache, then executes it, forwarding the arguments. `go tool` with no arguments lists the available tools: the ones shipped with the toolchain, plus the ones this module declares. That last part is what makes the directive useful in a check script — the script says `go tool thing -flag`, and it means the same build for every caller, with no setup step to keep in sync and nothing to add to `PATH`. ## What it pins and what it does not Be precise about the boundary, because it is where the remaining drift hides. - It pins the tool's **source version**. Two machines build the same code. - It does **not** pin the Go toolchain that compiles that source, and it does not pin the toolchain that formats or vets your own code. If the pipeline runs one Go release and the laptop runs another, output can still differ — most visibly for anything that emits formatted Go, because the formatter's output has changed across releases (Go 1.19's doc-comment reformatting is the well-known case). Pinning the tool and letting the Go version float only halves the problem. - It does not sandbox the tool. A tool that reads the environment, the clock, or the network can still produce machine-dependent output. ## The tradeoff worth naming The tool's dependencies become part of your main module's requirement graph. That is what makes a single, consistent resolution possible, and it is also the cost: a tool with a large dependency tree adds lines to `go.mod`, participates in version selection with your own requirements, and can pull your build list forward. For a tool used by exactly one repository this is fine and the bookkeeping is worth it. For a large tool shared across many repositories, some teams prefer to keep it out of the graph and pin it another way — and that is a genuine judgment call, not an oversight. ## What to say in an interview The short version: tools used by the build are dependencies, and dependencies belong in `go.mod` where they are versioned, hashed, reviewed and reproducible. The `tool` directive plus `go tool` is the toolchain's own answer to that, the `tools.go` blank-import file is the older answer, and an ambient binary on `PATH` is no answer at all.
- What did teams do before the `tool` directive existed, and why was it a workaround?They committed a `tools.go` file guarded by a build constraint so it never compiled into the binary, containing blank imports of the tool packages. The imports made `go mod tidy` keep those modules and versions in `go.mod` and `go.sum`. It pinned the version, but nothing about it produced a command that ran the pinned build — you still installed a binary separately and hoped it matched.
- What drift does the `tool` directive still leave in place?It pins the tool's source, not the Go release that compiles it and not the release whose formatter and vet run over your own code. Two machines on different Go versions can build the same pinned tool and still disagree, because the formatter's output and vet's analyzer set change between releases. It also does not stop a tool that reads the clock, the environment or the network from producing machine-dependent output.
- What is the cost of putting a tool in the module graph?The tool's dependencies join the main module's requirements, so `go.mod` grows and the tool's needs take part in version selection alongside your own. For a tool used by one repository that is a fair price for reproducibility. For a heavyweight tool shared across many repositories, teams sometimes keep it out of the graph deliberately rather than let it drag the build list forward.
saying these in an interview costs you the question
- Thinks a tool directive installs a binary during go build
- Says the version comes from whatever is on PATH
- Claims it also pins the Go release that compiles the tool
- Believes tools.go compiled into the shipped binary
- Ignores that the tool's dependencies enter go.mod