Vendoring in Go
Vendoring is the practice of committing copies of a project’s dependency source code into the project’s own repository, under a top-level
vendor/directory, so that builds consult that directory instead of downloading modules from the network or module cache. In module-aware Go, vendoring is produced and maintained bygo mod vendor, recorded in avendor/modules.txtmanifest, and — since Go 1.14 — used automatically whenever a consistentvendor/directory is present andgo.moddeclaresgo 1.14or higher. Vendoring trades repository size for build hermeticity: a vendored project builds with-mod=vendor, touching neither the module proxy nor any VCS, which makes builds fully offline, reproducible, and auditable in code review.
Mental Model
Think of vendor/ as a frozen, flattened snapshot of exactly the dependency source your module needs — no more, no less. It is not the module cache ($GOPATH/pkg/mod, a shared, content-addressed, multi-version store on your machine); it is a project-local, single-version-per-module tree that travels with your source code.
The mental shift from the old GOPATH-era vendoring is important. Pre-modules, vendor/ was the primary mechanism for pinning dependencies and Go would happily build whatever was in it. In module-aware Go, go.mod and go.sum are the source of truth for which versions and which hashes; vendor/ is merely a materialization of that decision. go mod vendor does not choose versions — Minimal Version Selection does — it just copies the chosen versions’ packages into the tree. This is why a vendor/ directory can become inconsistent with go.mod and why the go command checks for that.
flowchart LR A["go.mod / go.sum<br/>(source of truth:<br/>which versions)"] -->|"go mod vendor"| B["vendor/ tree<br/>(materialized source)"] A -->|"records"| C["vendor/modules.txt<br/>(manifest)"] B --> D{"go build"} C --> D D -->|"-mod=vendor<br/>(auto if go>=1.14)"| E["build from vendor/<br/>NO network, NO cache"] D -->|"-mod=mod / readonly"| F["build from module cache<br/>(may fetch via GOPROXY)"]
Figure: vendoring as materialization. go.mod decides versions; go mod vendor copies them into vendor/; modules.txt records the mapping. Insight: vendor/ is downstream of go.mod, never the other way around — if they disagree, the build fails rather than silently picking one.
The Mechanics of go mod vendor
Running go mod vendor in the main module’s root does three things (Go Modules Reference, “go mod vendor”):
- It computes the full set of packages needed to build and test the packages in the main module — the import graph, transitively closed, as resolved by minimal version selection.
- It copies those packages’ source files into
vendor/, laid out by module path: a packagegithub.com/pkg/errorslands invendor/github.com/pkg/errors/. Crucially, it copies only the packages actually imported — not entire modules. A module providing 50 packages of which you import 2 contributes only those 2 directories. It also copies non-Go files needed for the build (such as files referenced by//go:embed) and each module’sLICENSE-style files, but it prunes test files of dependencies, documentation, and unused packages. - It writes
vendor/modules.txt, the manifest.
modules.txt is the linchpin. It lists, for every vendored module, the module path, the exact version selected, an ## explicit marker if the module is a direct requirement in go.mod, the go version that module declares, and the list of packages vendored from it. A trimmed example:
# github.com/pkg/errors v0.9.1
## explicit
github.com/pkg/errors
# golang.org/x/sys v0.15.0
golang.org/x/sys/unix
# github.com/pkg/errors v0.9.1— the module and the version MVS selected.## explicit— this module appears in arequiredirective ingo.moddirectly (as opposed to being pulled in only transitively).github.com/pkg/errors— the one package vendored from that module.- The
golang.org/x/sysblock has no## explicitline, marking it a purely transitive dependency, and only itsunixsub-package was needed.
When a build runs in vendor mode, the go command does not re-resolve the module graph. It reads vendor/modules.txt, cross-checks it against go.mod for consistency, and resolves every import directly to a directory under vendor/. That is what makes vendored builds fast and offline: there is no graph computation, no proxy contact, no cache lookup.
The behavior of go mod vendor changed meaningfully at the go 1.17 line in go.mod (Go 1.17 release notes). At go 1.17 or higher, go mod vendor records the go version from each dependency’s own go.mod in modules.txt (the ## go X.Y annotation), so the build can apply the correct per-module language semantics without the dependency’s go.mod being present. And it deliberately omits the go.mod and go.sum files of vendored dependencies — that omission is what lets a go command invoked from inside a vendor/ subdirectory still correctly identify the outer main module rather than mistaking a vendored dependency for a separate module (Go Modules Reference).
go mod vendor also explicitly excludes test code of vendored packages — only the non-test source needed to compile is copied — and packages imported only by tests of modules outside the main module are left out entirely (go mod vendor help text). The command itself takes three flags: -v prints the names of vendored modules and packages to standard error (useful for auditing what was pulled in); -e tells it to proceed despite package-loading errors instead of aborting; and -o outdir writes the tree somewhere other than vendor/ — but note the go command will only consume a directory literally named vendor in the module root, so -o exists for other tools, not for relocating a working vendor directory.
When Vendor Mode Is Used — the -mod Flag
Three values of the -mod build flag govern dependency resolution (Go Modules Reference, “Build commands”):
-mod=vendor— usevendor/; do not access the network or the module cache. Imports must be satisfiable fromvendor/.-mod=readonly— ignorevendor/; use the module cache (fetching viaGOPROXYif needed); refuse to modifygo.modorgo.sum, erroring if they would need updating. This is the default when novendor/directory is present.-mod=mod— ignorevendor/; use the module cache; automatically updatego.mod/go.sumas needed.
The decisive rule, introduced in Go 1.14: if the go line in go.mod says go 1.14 or higher and a vendor/ directory exists in the main module root, the go command behaves as if -mod=vendor were passed (Go 1.14 release notes). Before Go 1.14, vendoring in module mode required an explicit -mod=vendor on every command. This auto-detection is why “I added a vendor/ directory and now my build ignores the network” surprises people — it is intentional and version-gated.
To override the automatic choice on a per-invocation basis:
go build -mod=mod ./... # ignore vendor/, use the module cache
go build -mod=vendor ./... # force vendor mode even if you'd otherwise not
GOFLAGS=-mod=mod go test ./... # persist the override for a shell sessionConsistency Checking
Because vendor/ is downstream of go.mod, the two can drift — someone edits go.mod (bumps a version, adds a dependency) but forgets to re-run go mod vendor. The go command guards against this. In vendor mode it verifies that vendor/modules.txt is consistent with go.mod: every ## explicit module in modules.txt is required by go.mod at the same version, and vice versa. If they disagree, the build fails with a message like:
go: inconsistent vendoring in /path/to/module:
github.com/pkg/errors@v0.9.1: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt
To ignore the vendor directory, use -mod=mod or -mod=readonly.
To sync the vendor directory, run:
go mod vendor
This is a feature: the build refuses to proceed with a stale snapshot rather than silently building something other than what go.mod describes. The fix is exactly what the message says — re-run go mod vendor and commit the result.
Consistency is checked against go.mod, but integrity — has any vendored byte been tampered with? — is a separate question answered by go mod verify. That command “checks that the dependencies of the current module … have not been modified since being downloaded,” printing all modules verified on success and exiting non-zero otherwise (go mod verify help text). The check works by re-hashing the source and comparing against go.sum. The hash is the h1: directory hash: the dirhash algorithm produces, for each file, a line <hex-SHA256-of-file-content> <filename> (two spaces), sorts those lines by filename, and takes the SHA-256 of the concatenation — base64-encoded behind the h1: prefix (golang.org/x/mod/sumdb/dirhash docs). Because only file names and contents enter the hash — never zip metadata, timestamps, or compression — the same h1: value is produced whether the source comes from a zip in the cache or from a directory under vendor/. That equivalence is precisely what lets go mod verify validate a vendored tree against a go.sum originally computed from a downloaded zip.
What Vendoring Does Not Change
A frequent surprise: enabling vendoring does not make every go subcommand offline. Only the build commands — go build, go test, go run, go vet, go list of packages — switch to reading from vendor/. The module-management commands behave identically with or without a vendor/ directory: go mod download, go mod tidy, and go get still “download modules and access the module cache” regardless (Go Modules Reference). This is by design — those commands exist to manipulate the module graph, which lives in go.mod/go.sum/the cache, not in the materialized vendor/ snapshot. It is also a common reason a “vendored” CI pipeline is not actually hermetic (see Failure Modes).
It is worth distinguishing the three storage locations a vendored project juggles. The module cache ($GOMODCACHE, default $GOPATH/pkg/mod) is a machine-global, content-addressed, multi-version store of downloaded module source plus a cache/download/ subtree holding the raw proxy responses (.info, .mod, .zip, .ziphash files). The vendor/ directory is a project-local, single-version-per-module, flattened copy. The build cache (see The Go Build Cache) holds compiled outputs, not source, and is unrelated to either. Vendor mode bypasses the module cache for builds but the cache is still where go mod download and go mod tidy operate.
Failure Modes and Common Misunderstandings
“I edited vendor/ directly and my fix disappeared.” go mod vendor overwrites the directory wholesale. Hand-editing vendored source is futile — the next go mod vendor erases it. To genuinely patch a dependency you must use a replace directive (or a workspace) pointing at a fork, then re-vendor.
Vendored tests are pruned. go mod vendor does not vendor the test files of your dependencies, and it prunes packages you do not import. So go test all (which would test every dependency) cannot run in a vendored, offline build — only your own tests can. This is by design: vendoring serves building your module, not exhaustively testing the universe.
go.sum is still needed. Vendoring does not make go.sum redundant. go mod verify checks the vendored tree against go.sum; tooling and go mod subcommands still consult it. Delete go.sum and you lose verifiability even with a full vendor/.
Repository bloat and noisy diffs. A large dependency graph can add tens of thousands of files. Every dependency bump produces a sprawling diff. Some teams find this desirable (every byte of dependency change is visible in code review — a real supply-chain control); others find it unbearable. There is no middle setting; it is vendor-or-not.
CI not actually offline. Teams vendor expecting hermetic CI but then run a go.mod-mutating command (go get, go mod tidy) in the pipeline, which ignores vendor/ and hits the network anyway. To prove hermeticity, build with GOFLAGS=-mod=vendor and GOPROXY=off so any accidental fetch is a hard error.
Alternatives and When to Choose Them
- Module proxy + cache (no vendoring) — the default. Builds fetch through the Module Proxy and Checksum Database and cache locally. Smaller repository, clean diffs, but builds depend on proxy availability and a populated cache. Best for most projects.
- Self-hosted module proxy — an internal Athens / Artifactory mirror gives availability and audit guarantees without committing dependency source. The usual enterprise answer when “build must not depend on GitHub” but “do not bloat the repo” both hold.
- Vendoring — choose it when you need guaranteed offline builds (air-gapped environments, regulated industries), when dependency source must be visible in code review for supply-chain auditing, or when you must build with toolchains/CI that have no network at all. The Go standard library and the Go command themselves vendor their dependencies for exactly this reason.
- Go Workspaces — solves local multi-module development, not distribution. Workspaces and vendoring are orthogonal; you can use both. A workspace can itself be vendored with
go work vendor, which “resets the workspace’s vendor directory to include all packages needed to build and test all the workspace’s packages” (go work vendorhelp text) — producing a singlevendor/tree at the workspace root rather than per-module. This is how a multi-module workspace gets a hermetic, offline build.
Production Notes
The go command’s own source tree vendors its dependencies (under src/cmd/vendor/), and the Go project vendors golang.org/x/... packages into the standard library — a deliberate choice so that building Go never requires the network. This is the canonical example of vendoring’s “right” use case: a tool that must bootstrap from source with zero external dependencies.
Practical guidance:
- If you vendor, commit
vendor/andgo.mod/go.sum/modules.txt, and rungo mod vendoras part of (or verified by) CI so the snapshot can never silently drift. A CI stepgo mod vendor && git diff --exit-code vendor/catches a stale tree. - Vendoring does not change which versions you get — that is still Minimal Version Selection over
go.mod. To upgrade, rungo get/go mod tidythengo mod vendor. - For large microservice repos the diff noise is real; many teams instead run an internal proxy and reserve vendoring for a small number of build-critical, must-be-offline components.
- Vendoring is not a security control by itself — it freezes bytes but does not verify them;
go.sumand the checksum database still do the verifying.
See Also
- Module Proxy and Checksum Database — the network path vendoring replaces
- Go Modules — the dependency model
vendor/materializes - go.mod and go.sum — the source of truth
vendor/is derived from - Minimal Version Selection — decides which versions get vendored
- Go Workspaces — the local-development analogue, orthogonal to vendoring
- The Go Build Cache — the compile-output cache, distinct from the module cache and
vendor/ - Go Internals MOC — §15, Modules, Dependencies, and Toolchain Management