LiteLLM Hijacked: 8 Months of Unsupervised AI Agent pip installs. My Findings

59 packages. No venv. No lockfile. No audit. A supply chain attack finally made me look at what was actually on my machine.

8 min read

Last Thursday, I asked Claude Code to build me a transcription tool. In 47 minutes, it installed 30 Python packages on my machine. torch, scipy, huggingface_hub, numba, tiktoken. And dozens of transitive dependencies underneath that I don't even know the name of. I approved every single installation without looking. As usual.

Then I see that litellm got hijacked. 97 million downloads per month, two compromised versions on PyPI, a .pth file that executes on every Python startup without import. SSH keys stolen, AWS tokens, GCP credentials, environment variables. Discovered only because the malware had a bug that crashed the machine.

I wasn't using litellm. But 59 packages sitting in global pip with no venv, no lockfile, no audit, after 8 months of daily dev with an AI agent? The next litellm was coming for me. I opened my terminal and looked at what was actually on my machine. Here's what I found, and what I fixed in 45 minutes.

TLDR: AI coding agents removed the last human friction between you and supply chain attacks. They install, you approve, nobody audits. I scanned my machine, found 59 unaudited global packages, migrated to uv with hash-locked dependencies, and added four lines to my CLAUDE.md that restore the friction. The whole thing took 45 minutes. Open your terminal and run pip freeze before your next coding session.

Two developers exploring software dependency risks with comic illustration
When your AI agent becomes the ultimate package manager rebellion 🤖🏴‍☠️

What Happened to LiteLLM (And Why It Should Scare You)

On March 24, a threat group called TeamPCP published litellm versions 1.82.7 and 1.82.8 on PyPI through a stolen maintainer account. Same group had previously compromised Trivy, a security scanner (yes, the people who hack security tools to hack your tools). The payload was a .pth file. If you don't know what that is: Python loads .pth files automatically at startup. No import needed. No trace in your code. Just having the package installed was enough.

The malware harvested SSH keys, AWS credentials, GCP tokens, Kubernetes configs, environment variables, and crypto wallets. It phoned home to an external server on every Python startup. 97 million downloads per month on that package.

It got discovered because the attacker made a mistake. The malware had a bug that caused a fork bomb, RAM usage spiked, machines crashed. A dev using Cursor noticed his machine was dying, traced it to a MCP plugin that pulled litellm as a transitive dependency. Transitive. He never installed litellm himself. His AI tool did, through a plugin, through a dependency chain he never reviewed.

One important caveat: the official litellm Docker image wasn't affected (pinned deps). The GitHub repo stayed clean. Only the PyPI distribution was compromised. But that's where most people get their packages from.

If the attacker hadn't crashed the machine, nobody would have noticed. For weeks. Maybe months.

I Asked My AI Agent to Scan My Own Machine

When I read about litellm, my first reaction was to check if I was hit. My second reaction was realizing I had zero tooling to do it.

So I did what I always do. I asked Claude Code. "Scan my machine for compromised Python packages." What followed was a bit embarrassing to watch. The agent started a recursive search, immediately hit timeouts because iCloud was syncing half my filesystem. Symlinks slowing down ripgrep everywhere. It had to navigate around macOS sandboxing just to look at my own site-packages. Like watching someone try to inspect their own house but the doors keep locking themselves.

The scan found 59 packages installed in global pip. Fifty-nine. I recognized maybe half of them. The rest were transitive dependencies from those 47 minutes of transcription tool building, plus months of other "sure, install that" moments accumulated on top.

Then I asked Claude Code to run pip-audit. Turns out pip-audit wasn't installed. Never needed it before (never thought about it before, more accurately). And this is where it gets properly ironic: to install pip-audit, Claude Code had to create a temporary venv because PEP 668 blocks global installations on modern Python. The same PEP 668 that the 30 whisper packages had bypassed with --break-system-packages three weeks earlier.

The audit tool was blocked by the system. The 30 unaudited packages had sailed right through.

pip-audit found one CVE. Pygments 2.19.2, low severity, a rendering library not exposed to the network. No litellm on my machine. Clean.

But clean by luck, not by design. And that's where the real problem starts.

Your AI Agent Removed the Last Friction That Was Protecting You

Before AI coding agents, you typed pip install yourself. It was a conscious moment. You saw the package name. You had at least one second to wonder if that was the right one, if you actually needed it, if maybe you should check the download count first. Most of the time you didn't check. But the friction was there. A speed bump between you and a potential supply chain compromise.

AI agents removed that speed bump entirely.

On my machine, I can trace back the two installation waves from the transcription project. First wave at 14:42, Claude Code tried openai-whisper. Second wave at 15:45, it switched to mlx-whisper because the first one didn't work well on Apple Silicon. Two full dependency trees layered on top of each other. I didn't review either one. I was probably making coffee during the second one.

This is consent fatigue applied to dependencies. The agent proposes, you hit "y", the packages flow in. Same mechanism that makes you accept cookie banners without reading them, except the cookie banner can't steal your SSH keys.

Andrej Karpathy's take after litellm was straightforward: "yoink with LLMs, fewer dependencies." And he's right on principle. Less surface area means less risk. But yoink doesn't solve the problem of dependencies you can't eliminate. litellm is not a 50-line utility you can copy-paste. Neither is torch. Neither is scipy. Some dependencies exist because the alternative is rewriting a decade of optimized C code, and nobody is doing that between two meetings on a Thursday.

I explored the 3-layer CLAUDE.md model (review, enforcement, intent) a few months ago. Turns out it was missing a layer: dependency security policy. The model tells the agent how to write code, but says nothing about what it's allowed to install.

Agents didn't create a new attack vector. Supply chain attacks existed long before Claude Code. What agents did is remove the friction that was protecting you from them. It's an amplifier, not a cause. But the consequence is identical.

The agent didn't create the vulnerability. It removed the friction that was protecting you from it.

Trust chain diagram showing: Dev approves agent → agent calls pip install → pip resolves dependency tree (invisible to dev) → each node in tree is a potential attack point. Visual contrast between "before agents" flow (conscious friction, dev sees package name) and "with agents" flow (automatic approval, dependency tree hidden). Flat geometric style.
Trust Chain: Before vs. After AI Agents

What I Changed in 45 Minutes (And What Actually Protects You)

First thing I did:

pip freeze > requirements-snapshot-20250325.txt

That's it. A snapshot of everything currently installed globally. Not a fix. A photograph of the crime scene before you start cleaning. Because you need to know what the mess looks like before you touch it.

Then I went through the snapshot and pinned every version exactly. No more >=. No more ~=. Every single version locked to its exact number. If you have requests>=2.28 in any file on your machine right now, you're telling pip "install whatever version you want as long as it's above 2.28." That includes a hypothetical compromised 2.32 that TeamPCP uploads next Tuesday.

Next: uv. Built by Astral (the people behind Ruff). I initialized my transcription project properly this time:

uv init transcription-tool
cd transcription-tool
uv add mlx-whisper typer

Two direct dependencies. uv resolved them into 63 packages. Installed 42 (the rest are platform-specific alternatives). And the part that matters: the lockfile contains SHA256 hashes for every single package.

The hash is the real protection. What happened with litellm: TeamPCP uploaded a new version with the same version number pattern to PyPI. If you were using pip with >=, you'd pull it automatically. But if you're using a lockfile with hashes, the content of the package doesn't match the recorded hash. uv sync refuses to install. Done. Attack blocked at the door.

pip has no equivalent mechanism by default. You can use --require-hashes, but you have to generate and maintain them yourself. uv does it automatically on every uv lock. Full disclosure: uv is a young tool (Astral released it in 2024), and the ecosystem around it is still moving fast. Pip with --require-hashes gives you similar protection if you prefer something more established. The important thing is the lockfile with hashes, not the specific tool.

Last step: venv isolation per project. Every project gets its own environment. No more global pip on a machine that has SSH keys, API tokens, and cloud credentials sitting in environment variables. I also added .nosync for iCloud because having your cloud service sync your venv while you're debugging dependency conflicts is the kind of experience you don't want to have twice.

Before: 30 packages in global pip. No trace of what installed them or when.
After: 2 dependencies. 63 resolved. Each one with a SHA256 hash. In an isolated venv that can't touch the rest of my system.

The Four Lines I Added to My CLAUDE.md

Tools are not enough if the agent installing packages for you has no rules. So I added four lines at the top of my CLAUDE.md:

1. Never run pip/npm install without explicit user approval
2. Before suggesting a new dependency, list its major sub-dependencies
3. Prefer generating utility code over adding a dependency when the functionality is under 200 lines
4. Always pin exact versions, never use >= or ~=

I already had "never install without asking" in my prompt contracts framework. That rule was necessary but not sufficient. I was still approving blindly because I couldn't see the dependency tree.

Rule 2 is the one that restores friction. When Claude Code tells me "mlx-whisper needs torch, numpy, scipy, huggingface_hub, tokenizers, and 25 transitive dependencies," I have a conscious moment again. I can decide whether a transcription tool is worth pulling half of PyTorch onto my machine. Maybe the answer is still yes. But at least I'm deciding, not rubber-stamping.

Rule 3 is the Karpathy principle with a guardrail. If the functionality is simple enough, generate the code instead of installing a package. But "simple enough" needs a boundary, otherwise you end up rewriting cryptography libraries from scratch (please don't). Two hundred lines is my threshold. Yours might be different.

Rule 4 is the most basic and the most ignored. Every >= in your project is a door you left open for someone else to walk through.

These four lines are not magic. A sufficiently capable agent could work around them. But for current production AI coding agents, it's the difference between installing blindly and installing knowingly. These rules don't protect against packages already compromised in your existing deps. They prevent future blind installations. Auditing what's already there (pip-audit, uv lock) is a separate step you still need to do.

The friction your agent removed, you put it back with four lines in your CLAUDE.md.


The next litellm is coming. Not a question of "if." And this time the attacker won't make a bug that crashes your RAM. The malware will be silent, the exfiltration quiet, and you won't know your SSH keys are gone until you see commits you never made.

Before you get hit by the next one, open your terminal. Run pip freeze. Count the packages you don't recognize. Install uv. Create a venv. Open your CLAUDE.md and add four lines before your next session. 45 minutes. Zero cost.

Your AI coding agent is exactly as secure as the rules you give it. No rules, no security. Just luck. And luck is not a plan.

Sources: LiteLLM supply chain compromise GitHub thread: github.com/BerriAI/litellm/issues/9484 | uv documentation: docs.astral.sh/uv

(*) The cover is AI-generated. My actual terminal output during the scan was way less photogenic.


Supply chain attacks are the new silent killer for AI dev workflows. Learn how to lock down your dependencies and protect your infrastructure.

Join the newsletter