LiteLLM is the adapter most AI teams reach for when they want one interface across OpenAI, Anthropic, Gemini and a dozen other providers. On October 21, version 1.107.0 of that package went up on PyPI carrying a credential stealer and a remote code execution backdoor, according to SafeDep’s analysis. All seven wheels across Linux, macOS and Windows installed three files into site-packages. Two of them run whenever any Python process starts on the host, including processes that never import litellm. PyPI has since pulled the release.

The part that should stop an AI engineer cold is what the stealer went after. SafeDep decoded cook.py and found a module list that reads like an inventory of the modern AI build environment: ai_devtools grabs tokens from Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, Gemini CLI, Codex auth.json, Continue, Aider, Goose, opencode, Qwen, Crush, Kiro, Zed and Hugging Face. A separate db_litellm module reads DATABASE_URL and dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which is where a LiteLLM proxy stores the LLM provider API keys for everything downstream.

This was not a smash-and-grab against random Python users. Someone built a payload that knows exactly what an AI team’s laptop and CI runner contain.

The mechanism is the story

The injection is almost elegant in how little it changed. The 1.107.0 source distribution’s pyproject.toml has one extra line in the maturin build section: data = "cook". That setting copies a cook/ folder into the wheel’s purelib, and pip installs it into site-packages. The sdist leaves the cook/ folder out, so the single line is the only visible change. No public commit in BerriAI/litellm contains data = "cook" or a cook/ folder.

The three planted files are cook.pth (12 bytes, import cook), cook.py (22,252 bytes, one layer of exec(zlib.decompress(base64.b64decode(...))) wrapping about 61 KB of Python) and sitecustomize.py (25,509 bytes, loader plus an embedded copy of the stealer). Because Python executes import lines in .pth files and imports sitecustomize at startup, both run in the application, in pip, in IDE language servers, in CI jobs. The stealer collects SSH keys, cloud credentials, CI/CD secrets, browser passwords, GPG and TLS private keys, then encrypts the haul with AES-256-CBC, wraps the key in RSA-OAEP against an embedded 4096-bit public key, and POSTs it to www.litellm[.]dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb.

The backdoor in sitecustomize.py does the persistence. It takes a lock on /tmp/remote_code_loader.lock, forks, detaches, and every 3600 seconds reads a server address from raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt, re-runs the stealer, and downloads and exec()s whatever http://<domain>/codepy returns. The attacker could repoint every infected host by editing one file. At 13:40 UTC the letters9813 account and its repository returned 404, about two hours after the release.

The build pipeline, not the token

SafeDep’s assessment is the uncomfortable part. The wheels match the output of BerriAI’s normal release pipeline: maturin 1.9.4, correct RECORD entries with no post-build edits, separate native builds at 11:20 (macOS), 11:21 (Linux) and 11:37 UTC (Windows). The Windows wheel’s 2,804 .py files all carry CRLF line endings, including the three implants, which is what a git checkout on Windows with line-ending conversion produces. No public GitHub Actions run built the release; SafeDep checked all 539 runs between 09:30 and 12:30 UTC.

The sdist itself does not match its tag. Tag v1.107.0 points to commit 66c8f5b at 06:59 UTC, and that commit’s pyproject.toml says version = "1.108.0". The sdist matches a later main commit, a8113b0 at 08:50 UTC, plus five edited files and the cook/ folder. Four of the edited files are Cargo.toml files gaining a readme = "README.md" line, which the legitimate 1.106.2 and rc2 releases also have. The fifth is pyproject.toml, edited to set the version back to 1.107.0, pin litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78 (the rc2 values, so a dependency diff against rc2 shows nothing unusual), and add data = "cook".

SafeDep concludes, with moderate to high confidence, that BerriAI’s private release pipeline built these wheels from an attacker-modified git source. A stolen PyPI token used from the attacker’s own machines fits the evidence less well. Only BerriAI’s pipeline logs can confirm it.

Why the AI stack is the soft target

LiteLLM is not a peripheral library. It sits in front of the model providers, which means it holds the keys to them. That is exactly why the db_litellm module exists and why the ai_devtools module covers more than a dozen agent and coding tools. The AI tooling ecosystem has grown fast enough that its dependency graph is now dense with packages that concentrate secrets: proxy layers, gateway services, eval harnesses, observability shims. Each one is a single install away from being a credential faucet.

The AI dev tools on that target list are also a new class of secret holder. A Cursor or Claude Code token is not just an API key; it can carry session context, workspace access, and billing. A Hugging Face token can reach private model repos. The economics of stealing them are obvious, and the defenses are not. Most of these tools store credentials in plain files under the home directory, which is precisely where dev_secrets and shell_hist went looking.

There is a quieter failure here too. PyPI has no provenance attestations for 1.107.0, 1.106.2 or rc2, per SafeDep. The litellm-dispatcher[bot] published the GitHub release at 11:50 UTC, about ten minutes after the PyPI upload, and the same delay occurred for the clean 1.106.2. The pipeline that publishes to PyPI and the pipeline that publishes to GitHub are not the same pipeline, and nothing in the public record ties a given wheel to a given commit.

What builders should take from it

The immediate triage is mechanical. If you installed litellm==1.107.0, check site-packages for cook.pth, cook.py and sitecustomize.py, check for /tmp/.pc_once_72129118c59f5636c0bb and /tmp/remote_code_loader.lock, and look for PRIVCOLLECT_ACTIVE=1 in running processes. Rotate everything the stealer touched: SSH keys, AWS and GCP credentials, kubeconfigs, GitHub tokens, CI secrets, and every LLM provider key in any LiteLLM proxy database. The clean versions are 1.106.2, 1.107.0rc1 and 1.107.0rc2, and the pinned companions litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78 are clean.

The structural lesson is harder. The AI industry has spent two years arguing about model weights, eval suites and safety policies while the actual attack surface sat in a build script and a .pth file. A package that fronts every major model provider got compromised at the release-pipeline layer, and the payload was custom-built to strip the AI toolchain.

What to watch is BerriAI’s pipeline logs. They are the only artifact that can tell the industry whether a maintainer credential, a private branch, or a build reference was the entry point. Until those logs are public, every team running a LiteLLM proxy is holding keys that a stranger may already have copied.