Vendoring — committing your dependencies' source code directly into your repository — trades registry-time risk for repository-time risk: it makes builds immutable and immune to upstream deletion or tampering, at the cost of hiding those dependencies from scanners and freezing whatever vulnerabilities they contain. Whether that trade helps or hurts depends almost entirely on whether your tooling still sees the vendored code afterward.
What vendoring actually is, per ecosystem
The word covers several mechanically different things:
- Go:
go mod vendorcopies every module dependency intovendor/, withvendor/modules.txtrecording versions. Builds use it automatically when the directory exists (-mod=vendoris the default since Go 1.14). This is the best-behaved form of vendoring in existence because the manifest survives. - Rust:
cargo vendorwrites crates into a local directory and prints the.cargo/config.tomlstanza that redirects the registry to it.Cargo.lockstill records exact versions. - JavaScript: committing
node_modules, or checking in tarballs and pointingpackage.jsonatfile:paths. Rare now, and the messiest variant — 500 MB diffs, lost provenance. - C/C++: the granddaddy. Copying
zlib/orlibcurl/intothird_party/because there's no package manager to argue with. Half the CVE remediation pain in embedded software traces to exactly this. - Git submodules/subtrees: vendoring with an audit trail; the pointer to upstream survives.
Where vendoring genuinely helps
Immutability. Once code is in your repo, nobody can change what your build consumes — not a hijacked maintainer account, not a registry compromise, not a re-published tag. When left-pad was unpublished in 2016 and broke half of npm, vendored builds didn't notice. When ua-parser-js shipped malicious versions in 2021, teams that vendored a known-good version and updated deliberately were structurally immune during the attack window.
Availability. Air-gapped builds, registry outages, packages deleted for legal reasons — a vendored tree builds on a desert island. For regulated environments that must demonstrate reproducible builds from archived source (medical devices under FDA premarket guidance, automotive under UNECE R155), vendoring is often the least-bad way to satisfy the "you must be able to rebuild what you shipped" requirement.
Review at import time. A vendoring workflow forces every dependency update through a pull request containing the actual code diff, not just a version bump in a lockfile. A reviewer looking at a dependency update PR in a vendored repo can see the new postinstall script or the surprise obfuscated blob. Almost nobody reads these diffs line-by-line, granted — but the option exists, and diff-size anomalies alone (a patch release that changes 4,000 lines) are a real detection signal.
Where it quietly hurts you
Scanners stop seeing dependencies. This is the big one. Most SCA tools key off manifests — package-lock.json, go.mod, requirements.txt. Code copied into third_party/ with no manifest is invisible to manifest-based scanning, so your dashboard says "no known vulnerabilities" while a 2019 copy of zlib with CVE-2022-37434 sits in the tree. Detection requires file-signature or code-similarity scanning: syft picks up Go vendored packages fine (because modules.txt exists) but raw copied C sources need something like Trivy's or a commercial engine's code-matching. If your scanner can't enumerate your vendored code, you don't have a vulnerability management program for it — you have a folder. This is precisely the gap snippet-level SCA exists to close.
Updates get expensive, so they stop happening. The dependency that took npm update now takes: fetch upstream, re-apply local patches, re-vendor, review a 10,000-line diff, merge. Every step is friction, and friction compounds into staleness. In audits of vendored third_party/ directories I've been part of, the median age of vendored C libraries was over three years. Vendored code doesn't decay louder than registry code — it decays silently, because no npm audit ever mentions it.
Local patches fork you. The moment someone edits vendored code in place ("just a small fix"), you own a fork with no name. Upstream security patches no longer apply cleanly, and no CVE database has an identifier for zlib-but-with-jims-patch. If you must patch, keep patches as separate files (patches/0001-fix-foo.patch) applied at build time — pnpm patch and patch-package formalize this for npm; quilt-style patch dirs do it for C.
License and origin tracking degrades. An SBOM built from manifests can't attest code with no manifest. For a compliance program this is a direct finding: NTIA minimum elements require supplier and version per component, and "some files we copied in 2021" satisfies neither. Tooling like SBOM Studio can reconcile vendored trees against known-package fingerprints, but it's remediation for a problem you chose.
Rules that keep vendoring safe
If you vendor, do it with the machinery that preserves visibility:
- Always keep the manifest.
go mod vendorandcargo vendorretaingo.mod/Cargo.lock— the scanner still knows exact versions. Never vendor in a way that discards version metadata. - One directory, declared. All vendored code in
vendor/orthird_party/, each component in a subdirectory with anUPSTREAMfile: source URL, exact version or commit, retrieval date, local patches applied. Five lines that save a week during incident response. - No in-place edits. Patches live in
patches/, applied at build. The vendored tree stays byte-identical to upstream so hash-based identification works. - Automate re-vendoring. A scheduled job (Renovate handles
go mod vendorrepos natively) that re-vendors on upstream releases and opens a PR. Vendoring without an update pipeline is how you end up shipping 2019. - Scan the tree, not just the manifest. Verify your scanner actually reports findings in vendored paths — some tools exclude
vendor/by default as noise. Run a test: vendor something with a known CVE and confirm it appears.
The one-line summary: vendoring moves trust from the registry to your repo. That's a good trade only if your repo gets at least the same scrutiny the registry did.
Frequently asked questions
Is vendoring safer than a lockfile with integrity hashes?
For tamper-resistance, they're nearly equivalent — a lockfile's sha512 integrity fields pin content just as hard as committed bytes, with far less repo weight. Vendoring adds availability (builds survive registry deletion or outage) and import-time code review; lockfiles keep scanner and update ergonomics. Most teams should prefer lockfiles plus a caching proxy like Artifactory, and reserve vendoring for air-gap or regulatory requirements.
Does vendoring hide malicious packages better or worse?
Worse for the initial import, better afterward. A malicious version you vendor is now in your repo where code review and code-scanning can catch it, and it can't self-update. But if it slips in, no registry-side takedown or advisory-feed match will save you automatically — detection depends entirely on your own scanning of the vendored tree.
How should vendored code appear in an SBOM?
As first-class components with their real upstream identity — supplier, name, version, and ideally a purl (pkg:golang/github.com/foo/bar@v1.2.3), not as anonymous files of your application. Go and Cargo vendoring make this automatic because the metadata survives; hand-copied code needs an UPSTREAM manifest so SBOM generation has something to read.
When is vendoring clearly the wrong call?
Fast-moving ecosystems with good lockfile integrity and frequent security releases — JavaScript and Python foremost. There, vendoring's update friction directly translates to running known-vulnerable versions longer, and the availability benefit is better obtained with a pull-through registry cache. Save vendoring for Go, Rust, C/C++, and environments that must build offline.