Engineering8 min read

How to protect your projects from supply chain attacks

Supply chain attacks hit axios, TanStack, and PyPI this year. Three package manager settings in uv and pnpm, exact version pins, a dependency cooldown, and locked installs, close the main attack paths.

The morning the TanStack packages on npm were compromised, I was building React components with TanStack for an internal dashboard at my company. The malicious versions were live for a window of only around 20 minutes, and I missed it through luck, not through good configuration. Since that day I treat dependency security as a project requirement, the same as version control or tests.

The attacks keep coming. axios in March. The TanStack ecosystem in May, which spread into PyPI through the mistralai package. LiteLLM earlier this year. The protection costs you a few lines of package manager config. You need three settings for Python projects using uv, the same three ideas for JavaScript projects using pnpm, and one instruction for your AI coding agents. Everything below is copy-paste.

Installing a package is running arbitrary code

Every time you install an external package, through pip, uv, or npm, you let code someone else wrote execute on your machine. We've used package managers for so long that this stopped registering. It should register. When the source of your packages can't be fully trusted, you're playing Russian roulette with your system.

The attack follows a predictable chain. A maintainer clicks a phishing link, or their CI tokens get stolen, or an attacker publishes a package with a name one character off from a popular one. The compromised package lands on the official registry, npm or PyPI. Then you, or your AI agent, installs it. The payload scrapes SSH keys, environment variables, API keys, anything it can reach, and ships all of it to the attacker's server.

I grew up playing RuneScape, where you learn fast that you can never trust people on the internet. Yet somewhere along the way we all started trusting requirements.txt files we never read. Almost every tutorial opens with "pip install these 20 packages". That default lowered the barrier to entry for beginners, and it also trained an entire industry to stop questioning what enters their systems.

The worm that crossed from npm into PyPI

The TanStack attack was a worm, and that's what makes it scary. Once it landed on a compromised developer's machine, it searched the file system for CI/CD tokens and used them to publish copies of itself to other packages that developer maintained. That self-propagation is how it crossed ecosystems and ended up inside the mistralai package on PyPI.

That crossing matters if you write Python. The headline attacks have historically lived in npm, and Python developers mostly watched from the sidelines. With credentials as the transport mechanism, the registry boundary no longer protects you. PyPI had its own direct incident too. LiteLLM was compromised back in March.

Ignoring this is irresponsible at this point. The window between a malicious publish and detection can be minutes, my TanStack morning proved that, and you will not out-react an automated worm by reading security news faster.

Slopsquatting aims at your AI agents

Attackers now publish packages with names deliberately close to real ones and wait for AI agents to hallucinate the name into a project. The term for this is slopsquatting. I wasn't aware of it either until researching this topic, but it's real, and it fits how we work now. An agent runs in a long autonomous loop, decides it needs a package, slightly misremembers the name, installs the attacker's version, and you're hit.

The newer payloads go further and target the agents themselves, Claude Code, Gemini, whatever harness you run. Instead of relying only on pre-written regular expressions to find common credential patterns, the malware can invoke the agent CLI on your machine and let your own AI crawl your files for secrets.

Your agent is part of your attack surface now. The settings below limit what reaches your machine; the last section deals with the agent behavior itself.

Tip 1: pin exact versions

First, stop using pip directly and switch to uv. It's becoming the industry standard, the big labs use it, and it supports project-level security settings that pip doesn't. If you're learning Python for AI work, start with uv from day one.

By default, uv add pydantic writes the lower bound pydantic>=2.13.4 into pyproject.toml. That constraint accepts every future version, so newer, riskier code can flow into your project without you deciding anything. Switch the bound to exact:

toml
# pyproject.toml
[tool.uv]
add-bounds = "exact"

From then on, uv add pydantic writes pydantic==2.13.4. Nothing new comes in until you explicitly upgrade. You can verify the behavior by removing a package and adding it again. The constraint flips from >= to ==.

Tip 2: add a dependency cooldown

This is the most critical setting of the three. exclude-newer tells uv to ignore any package version published more recently than the window you set:

toml
# pyproject.toml
[tool.uv]
add-bounds = "exact"
exclude-newer = "7 days"

When a package gets compromised, the malicious version usually gets noticed, reported, and pulled within the first 24 hours. A 7-day cooldown means the bad version is gone long before your resolver would ever consider it. Tune the window to your own risk tolerance; the value accepts plain durations like "24 hours" or "30 days".

Don't blindly trust a dude on the internet on this one, me included. Verify it on your own machine. Set exclude-newer to an extreme value like "100 days", then re-add a package. When I did this with Pydantic, uv resolved 2.12.5 instead of the current 2.13.4, an older version from outside the window. That's the setting working.

Put these settings in pyproject.toml so they live in version control and apply to everyone on the project. uv also supports a user-level uv.toml if you want a machine-wide default; the official documentation covers both.

GenAI Accelerator

The gap between a demo and production

Anyone can wire up an LLM call. The real skill is designing, evaluating, and shipping systems that hold up.

See Curriculum

Tip 3: make the lockfile the contract

Every uv operation maintains uv.lock, the ground-truth snapshot of every resolved version. The default uv sync takes whatever is in pyproject.toml and installs it. Add --locked and uv refuses to proceed whenever the lockfile and pyproject.toml disagree:

Shell
uv sync --locked

To see what this protects against, edit pyproject.toml by hand and change a version number, which is exactly what a compromised AI agent or an attacker with file access would do. Run uv sync --locked and you get an error instead of a silent install. Use it locally, and definitely use it in CI/CD where nobody is watching the resolution output.

The same three protections in pnpm

The JavaScript ecosystem is where these attacks started, and pnpm ships direct equivalents of all three settings. They go in pnpm-workspace.yaml:

YAML
# pnpm-workspace.yaml
savePrefix: ""            # exact versions instead of ^x.y.z
minimumReleaseAge: 10080  # minutes; 10080 = 7 days
minimumReleaseAgeStrict: true

savePrefix: "" is the add-bounds = "exact" equivalent. pnpm add writes 5.2.1 instead of ^5.2.1. minimumReleaseAge is the cooldown, measured in minutes, so 10080 buys you the same 7 days. The strict flag removes the escape hatches, so the window holds even for transitive dependencies.

For the lockfile contract, run:

Shell
pnpm install --frozen-lockfile

It fails the install whenever pnpm-lock.yaml is out of sync with package.json, the same behavior as uv sync --locked. pnpm already defaults to it in CI environments, but make it explicit so nobody can talk a script out of it.

The site you're reading runs this exact setup with minimumReleaseAge: 1440 and strict mode on, so every package has to be at least 24 hours old before pnpm will touch it. pnpm 11 now ships that 24-hour cooldown as the default for everyone, which tells you where the ecosystem is heading. pnpm also blocks dependency install scripts unless you allow-list them, and install scripts are the main way npm payloads execute, so keep that list short.

Give your coding agents dependency rules

The settings above protect the install path. They don't change the behavior that triggers installs in the first place, and increasingly that behavior comes from AI agents working unattended. An agent that defaults to importing a new package for every piece of functionality multiplies your exposure, especially when you run it in long loops and let it solve problems for hours on end.

Tell it not to. Add a dependency section to your AGENTS.md or CLAUDE.md:

markdown
## Dependencies
 
- Never add a new dependency without asking first.
- Every dependency has to earn its place. If we only need one
  function from a library, propose rebuilding that function
  instead of importing the package.
- Never run `uv add`, `pip install`, or `pnpm add` on your own.
- When proposing a package, give the exact pinned version and a
  one-line reason it beats writing the code ourselves.

Rebuilding deserves more use than it gets. If you need one small component from a library, point the agent at the GitHub URL of the specific function or class and let it recreate that piece inside your codebase. You skip the dependency entirely and you own the code. Why import thousands of lines to use fifty?

This is a narrative shift more than a technical one. Projects used to start with installing 20 packages; I'd rather they start with zero and force each one to justify itself. The same discipline applies whether a human or an agent is typing the install command, and it's part of the same engineering mindset I cover in how to build production AI systems. If you're running agents as part of a personal AI operating system, these rules belong in its context files too.

With the three settings in place, the odds of an install-time attack reaching you drop close to zero. Not to zero. The residual risk is exactly why the dependency-discipline instruction matters. The cheapest package to secure is the one you never installed.

FAQ

Is pip still safe to use?

You can use it, but I recommend switching to uv. pip added a cooldown flag (--uploaded-prior-to) in version 26.1, but uv puts all three protections in pyproject.toml where they're versioned with the project, generates a lockfile by default, and has a better developer experience on top.

Does a dependency cooldown delay security patches?

Yes. A fix published yesterday waits out the same window as everything else. uv supports per-package overrides through exclude-newer-package when you need to pull one patched version through early, and pnpm audit --fix writes the same kind of exception. For most projects the tradeoff still favors the cooldown.

What is slopsquatting?

Slopsquatting is publishing malicious packages under names that AI models are likely to hallucinate, slight variations of real package names. When a coding agent invents the name and installs it, the attacker's code runs with your permissions. A dependency cooldown plus explicit agent rules about installing packages covers most of the risk.

Do these settings cover transitive dependencies?

Mostly yes. uv's exclude-newer applies to the whole resolution, and pnpm's minimumReleaseAge applies to transitive dependencies as well. Exact version bounds only pin your direct dependencies, but the lockfile pins the full tree, which is why the locked install is part of the set.

Written by

Dave Ebbelaar

Dave Ebbelaar

Senior AI Engineer

AI engineer and founder of Datalumina. Dave helps developers build production AI systems and turn technical skills into client work.