On 21 October one of my scheduled agents did exactly what it was told. It started a memory tool for itself, the way it does every run. That afternoon the tool brought a credential stealer with it.
Nothing alerted me. I found out five days later because my OpenRouter key was spending money on models I never use.
What happened
The memory tool was an MCP server launched with uvx basic-memory mcp. No version pin. Every start, uvx asks PyPI for the newest compatible versions of the tool and everything it depends on, and one of those dependencies is litellm.
At 11:39 UTC that day a poisoned litellm 1.107.0 went up on PyPI. It was not built by the maintainers. An attacker had been inside their GitHub organisation for eleven days, through a key that leaked in May and was never revoked, and used it to ship a release that looked completely normal. It was live for about two hours and was downloaded around 49,000 times.
My cron agent started at 14:20 local time, 12:20 UTC. Forty minutes after the release.
What it did
The package carried two files that Python runs on its own, without anyone importing anything:
cook.pth. Python executes import lines in.pthfiles on every interpreter start. This one collected environment variables,.envfiles, SSH keys, AI tool tokens, saved browser passwords and shell history, encrypted the lot and sent it to a lookalike domain.sitecustomize.py. It forked a detached process that checked a file on GitHub every hour for the address of more code to run. The attacker had left that file empty, so it never fetched anything. It still sat on my laptop polling for six days.
The agent runs as my user, and my user can read every key the agents use. So the working assumption is simple: every secret on that laptop was copied on 21 October.
How I found it
Not with a security tool. The OpenRouter key was being resold and used for models my setup never calls. That was the only signal, and it arrived five days late.
My first theories were wrong. I suspected the Windows desktop and an old backup, and spent time proving both clean. The answer was in the uv cache on the laptop the whole time, with a timestamp that matched the cron run.
What I did
- Killed the loader process, after saving evidence.
- Deleted the infected files from the uv cache and reinstalled the memory tool at a pinned version, with a constraint that keeps litellm below the bad release.
- Checked the sudo and login logs back to the 21st for anything that went past my user account. I found nothing.
- Started rotating every key the account could read. That is the slow, boring part and it is not finished.
What I am changing
The agents stay autonomous. I am not going to approve every package install by hand, because then the agents are pointless. The changes go around them instead:
- Make fresh releases wait seven days before anything installs them. This alone would have stopped it: the bad release was on PyPI for less than two hours.
- Lock the full dependency tree of every tool an agent can launch. Pinning the tool itself is not enough. litellm came in two levels down.
- Make the keys worth less if they leak again: one key per job, a spend limit and a model allowlist on each, and an hourly check that alerts when spending looks wrong.
- Run the tools agents launch in a box that cannot read my SSH keys, browser profile or other projects' secrets, one tool at a time, tested so the agents still run unattended.
- Have an agent watch the early signals every hour and compare them against what is actually running here. The first public warning about this release was a GitHub issue 27 minutes after it went up. The official advisory databases took more than ten hours.
The lesson I keep coming back to is the same one the attacker used on the maintainers. A key that might have leaked is a key that leaked. Rotate it.