Why Your Lock File Is the Most Important File in Your Repo
In 2021, the ua-parser-js npm package was compromised. Versions 0.7.29, 0.8.0, and 1.0.0 were published with cryptocurrency mining malware baked in. If your project didn't pin its dependencies — and many didn't — you might've pulled in the malicious version automatically on your next npm install.
That incident wasn't unique. The colors and faker sabotage, the event-stream backdoor, the peacenotwar protestware — the JavaScript ecosystem has had a rough few years. And it's not just npm. PyPI, RubyGems, and even Go modules have seen similar supply chain attacks.
Dependency management isn't glamorous work. But getting it wrong can be catastrophic.
Lock Files: What They Do and Why You Must Commit Them
A lock file (package-lock.json, yarn.lock, pnpm-lock.yaml, Pipfile.lock, poetry.lock, go.sum) records the exact version of every dependency in your tree — direct and transitive.
Without it, npm install on two different machines at two different times might produce different node_modules trees. Your package.json says "lodash": "^4.17.0", which means "any version from 4.17.0 up to but not including 5.0.0." Today that resolves to 4.17.21. Next month it might resolve to 4.17.22. Usually that's fine. Sometimes it's not.
Always commit your lock file. Here's why this keeps coming up:
- CI builds must match local builds. Without a lock file, CI might install different versions than your laptop.
- Debugging becomes impossible if you can't reproduce the exact dependency tree from a given commit.
.gitignoretemplates from years ago sometimes included lock files. Remove that line.
Version Ranges and What They Actually Mean
Semver is a contract, not a guarantee. Here's what each range operator does in npm:
"exact": "4.17.21" → only 4.17.21
"patch": "~4.17.21" → >=4.17.21 <4.18.0
"minor": "^4.17.21" → >=4.17.21 <5.0.0
"any": "*" → anything (don't do this)
"range": ">=2.0.0 <3.1.0" → explicit bounds
The ^ (caret) is npm's default and it's fine for most packages. But for critical infrastructure dependencies — your ORM, your HTTP framework, your auth library — I'd argue you should pin exact versions and update them deliberately. The minor convenience of auto-updating isn't worth the risk of a breaking change sneaking into your build.
The Python Side
Poetry handles this well with pyproject.toml specifying ranges and poetry.lock pinning exact versions. pip's requirements.txt has no range operator built in — you need pip-tools to get a proper lock workflow:
# requirements.in (what you want)
flask>=3.0,<4.0
sqlalchemy>=2.0
# Generate pinned requirements.txt
pip-compile requirements.in
# requirements.txt (what you install)
flask==3.1.0
sqlalchemy==2.0.36
# ... all transitive deps pinned
Automated Vulnerability Scanning
Every project should run dependency audits in CI. The bare minimum:
# npm
npm audit --audit-level=high
# Python
pip-audit
# Go
govulncheck ./...
# Ruby
bundle-audit check --update
GitHub's Dependabot and Snyk both provide automated PR creation for vulnerable dependencies. Dependabot is free and built into GitHub — there's no reason not to enable it. But here's the thing: don't just auto-merge Dependabot PRs. Review them. Check the changelog. Run your tests. A version bump that "fixes a vulnerability" might also change behavior your code depends on.
Supply Chain Security Measures
Beyond auditing known vulnerabilities, consider these defenses:
Use a private registry or proxy. Artifactory, Verdaccio, or even npm's scoped registries let you cache and scan packages before they reach your developers. If a package gets compromised upstream, your proxy still serves the cached safe version.
Enable npm provenance. Since npm v9.5, packages can include provenance attestations linking the published package to its source repo and build process. Check for the "provenance" badge on npmjs.com before adding new dependencies.
Review new dependencies before adding them. Check: how many maintainers? When was the last release? How many downloads? Is there a funding model? A package with one anonymous maintainer and 12 weekly downloads is riskier than one backed by a company with thousands of dependents.
Use Socket.dev or similar tools. These analyze package behavior — detecting if a dependency reads environment variables, makes network requests, or accesses the filesystem in unexpected ways. It's a step beyond vulnerability scanning into behavioral analysis.
Keeping Dependencies Updated
The worst security posture is running dependencies so old that updating them requires a major effort. Then teams never update, and vulnerabilities accumulate.
Set a cadence. Monthly works for most teams. Use npm outdated or poetry show --outdated to see what's behind. Group updates by risk: patch versions first (low risk), minor versions next (medium risk), major versions last (review breaking changes carefully).
And please, don't have 200 direct dependencies. Every dependency is an attack surface. Before adding a package, ask: can I write this in 20 lines instead? Often the answer is yes.