Blog
>
The Ghost in the Skills

The Ghost in the Skills

Public AI agent skills instruct agents to install packages that were never published. The names sit unregistered, allocation is first come first served, and the agent does not hesitate. A supply chain attack nobody had to break-in to stage.

Metano Labs
AI agent skills are installing packages that don’t exist

A supply chain attack normally requires a compromise. It starts with either breaching a maintainer account, slipping a commit into a dependency, or poisoning a build server. It requires something to be broken into.

In the agentic era, an agent skill is a document an agent executes. Subverting one takes a free account and five minutes: the skill instructs the agent to install packages that were never published. These package names sit unregistered, and allocation on package repositories like PyPI, npm, RubyGems, etc is first come, first served. Whoever claims the name inherits every agent that has been told to install that package, and gets code execution on those machines.

Nothing is broken or breached. Every party in this chain behaved reasonably, and that is precisely the problem.

To demonstrate, we work through one skill in detail because the mechanism is easier to see in action with a single case. Though don’t be mistaken that this is an isolated case. The same defect appears in many skills across unrelated collections, on more than one marketplace, and the conclusions apply to any skill that ships install instructions.

This is not slopsquatting

Squatting names a model invents is a known problem, usually called slopsquatting. The attacker guesses which package a model will hallucinate, registers it, and waits. It works, but it is a probability game: the name is generated at inference time and the distribution shifts with the model, the prompt, and the temperature.


Nothing we discuss here is generated at inference time. The name is written down, in a file that is version-controlled, starred, forked, mirrored to marketplaces, and read out to a machine with no opinion about whether it looks plausible. The attacker is not betting on a distribution. The attacker is reading a file that says which name to take.

1.  The check that failed: unresolved install lines in a public agent skill

While most public agent skill scans yield nothing but noise, this one stood out for a remarkably mundane reason.

The skill is called graphql-audit, and it lives in one of the popular bug bounty skill collections on GitHub. It is genuinely good work: a structured GraphQL testing methodology covering introspection, field discovery, injection, authorisation. Near the top there is a setup table, the kind every skill has, telling the agent what to install before it starts.

Four PyPI packages to install. One question to answer: Do all of these packages exist?

Skill Tracer answers the question.

Figure 1. SkillTracer flagged them during the scan. Every install line in the skill, plus one from an unrelated repository, checked against the PyPI JSON API on 28 July 2026.

Out of the four, two didn’t exist, and another resolves to a suspicious package. These are the ghost dependencies. The non-existent packages graphw00f and gqlmap can be registered by anyone, thereby taking control of what gets installed as part of the skill execution. On the other hand, the graphql-cop resolves to  a package having unrelated lineage and history.
These could become a potential supply chain attack hiding in a popular skill.

Figure 2. graphql-cop on PyPI: a single 1.3 KB release dated 10 July 2026, no repository, and a maintainer with no visible connection to the upstream project.

This is not an isolated case. The pattern is seen in multiple other skills as well. A skill in a different collection tells the agent to run pip3 install jefferson ubireader. Both are real firmware forensics tools. jefferson resolves. The other does not, because the package has always been published as ubi_reader. One underscore. The name in the skill has never existed.

This is not confined to PyPI packages. Another skill named evolve directs the host agent to run `npx metaharness-darwin`, and guess what, metaharness-darwin npm package did not exist when we scanned. This is the cleanest execution path of the set: npx fetches and runs in one step, so whoever claimed the name would get execution the first time the skill reached that line. We found multiple similar examples across different repositories of different types of packages.

Figure 3. SkillTracer flagged the non-existent npm package during the scan of the skill.
What can go wrong?

These are not the same mistake, and the difference matters if you want to catch them at scale.

  • Real tool, wrong shelf. graphw00f is a genuine and well-regarded GraphQL fingerprinting tool. Its author ships it by git clone and has never published it to PyPI. The install line was always going to fail, and the name was up for grabs on PyPI the entire time.
  • Real tool, near-miss name. ubireader against ubi_reader. The classic typosquat target, except nobody has to make the typo, because it has already been made and committed to a public repository as part of the skill. Though this one is hard to exploit because PyPI normalises away separators when checking new names, so ubireader collides with ubi_reader and cannot be registered at all.
  • Wrong name, wrong shelf. gqlmap is a contraction of GraphQLmap, a widely used GraphQL pentesting engine with around 1,700 stars. Its author ships it by git clone and has never published it to PyPI. Two failures in one line: the name is wrong, and the registry was never the right place to look.
  • Right name, wrong owner. graphql-cop is a real GraphQL auditor by the same author as graphw00f, installed from requirements.txt or Docker, never by pip install graphql-cop. The name is nonetheless occupied on PyPI by a single 1.3 KB release held by an account with no visible connection to the upstream project. In addition, that name is not idle. PyPI recorded 2,079 downloads of graphql-cop in the 30 days to 29 July 2026, and 94 on that day alone, mirrors excluded. Every one of those is a resolution that reached a stranger's package instead of the tool the skill named.

Why is it worse than Typosquatting

A typosquat waits for a human to slip. Most humans do not. This one doesn’t wait. The mistake is already written down, in a file that is version-controlled, starred, forked, mirrored to marketplaces, and read out to a machine that has no opinion about whether a package name looks right. The install line fails deterministically, every time the skill runs. Whether that failure becomes a compromise depends only on whether anyone has claimed the name yet.

2.  The ninety-second attack: how package namesquatting works

We want to be explicit about the minimal effort required here, especially since the security industry is conditioned to look for complex operations, yet there is absolutely none to be found.

Figure 4. The complete resource requirement for taking over one of these names.

There is no vulnerability in this attack. Nothing is exploited. You create a free account, you claim a name nobody owns, you upload an archive. Every one of those actions is the registry working exactly as designed, and it has worked that way for years without anybody considering it a flaw.

What makes it pay is the last panel. Installing a Python package is not a download, it is an execution. A source distribution runs its build logic on the installing machine, as the installing user, before any code is imported. By the time anything could inspect the package, the package has already run.

And it runs somewhere good. A developer workstation with an agentic coding tool on it holds cloud credentials, SSH keys, signing material, source for things that have not shipped, and an API token for the agent platform itself. On this particular skill it also holds engagement scopes and client target lists, because the people who install GraphQL attack tooling are people who are paid to attack things.

The population most exposed to this is people who hunt vulnerabilities for a living, on machines loaded with everything required to do it.

The delivery mechanism is ordinary namesquatting. What is unusual is that the victim population selected itself: the skill's own subject matter did the filtering.

3.  Why AI agents fall for ghost dependencies
Figure 5. The same install line, read by two different kinds of reader.

Every property that makes an agent useful makes it worse at this one decision.

It carries no prior about which packages are real. It does not pause at an unfamiliar name. It was handed a document by a source it has been told to trust and asked to perform the setup steps, and performing setup steps is the entire job. A developer who ran pip install gqlmap and watched it fail would go looking for the real tool. An agent installs whatever answers to the name.

Figure 6. From published skill to code execution. No step requires compromising anything.

Autonomy widens it further. Our scan scored this skill to be highly autonomous, with the note that it presents a checklist and never requires human confirmation between phases, so a host agent can run the whole thing in sequence. Install is phase one. Nobody is watching phase one. Nobody has ever watched phase one.

4.  Containment: defensive holds on unclaimed package names

Publishing a list of live unclaimed names that public skills instruct agents to install would be publishing a target list. So before this article was written, our labs team registered graphw00f and gqlmap (and other dangling packages we discovered like metaharness-darwin), to take them out of circulation.

Every name we registered has received downloads since we took it. None of these is a real tool, none is referenced by anything other than the skills that named it, and none of them should be resolving to anything at all. We are not claiming those downloads are all via agents. Newly published packages attract automated scanners, and we have not separated that traffic. The point here is, each of these names is being resolved by something on a recurring basis, and until we registered them the answer to that resolution was whatever anyone chose to put there.

None of these packages registered by our labs contain working code. Each carries a description stating that it is a hold on an unclaimed name referenced by a public agent skill, that it is not the real tool, and that it collects nothing. Each lists a contact address. All are MIT licensed, and all will be transferred to the legitimate tool authors on request, without conditions, under PyPI's name transfer policy Disclosure. We notified the tool authors of the package names we have reserved, offering unconditional transfer of the package names on request. We hold no functional code and collect no data. No trademark holder is affiliated with or endorses these holds.

5.  Blast radius: forks, mirrors, and re-execution

The collection this came from is not obscure. Roughly five thousand stars, more than seven hundred forks, republished across multiple third-party skill marketplaces, installed with one git clone into the agent’s skills directory.

The fork row is the one to sit with. A maintainer who reads this and fixes their install lines tomorrow has fixed their repository. They have not fixed the several hundred copies, and they have no mechanism to.

6.  How to secure agent skill install lines
If you consume agent skills
  1. Resolve every install line you have loaded. Extract the names, check each one exists, check the thing at that name is the thing the skill claims. It is one API call per name and it belongs in CI, not in a person’s good intentions. For example, you can use these commands for PyPI and npm:
    curl -s -o /dev/null -w "%{http_code}\n" https://pypi.org/pypi/<name>/json curl -s -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/<name>
  2. Pin and hash. An unpinned install is a standing promise to execute whatever occupies that name on the day it runs. Hash-pinned lockfiles convert a hijacked name from a breach into a build failure.
  3. Stop letting agents install into the host. Setup steps should resolve inside a container or a virtual environment that is not the developer’s primary identity, with no ambient cloud credentials and no forwarded SSH agent.
  4. Rank install lines above prompt injection. Injection gets the conference talks. An unpinned install line is a well-understood path to code execution, and it is sitting in the setup section in plain sight.
If you publish agent skills
  1. Verify your own install lines before release. Every name, against the registry you named. If your tool is not on PyPI, do not write pip install. Write the clone.
  2. Claim your own names. If your project is popular enough that other people write install instructions for it, register the obvious names on the obvious registries yourself. It is free and it takes ninety seconds.
  3. Assume your forks never update. Your correction reaches your watchers. It does not reach the forks and it does not reach the marketplace that mirrored you last month.
If you run a registry or a marketplace
  1. Resolve install lines at ingestion. A marketplace publishing a manifest that says pip install <nonexistent> is distributing an unresolved install instruction, and can detect it with one API call per name.
  2. Alert on recently created single-release placeholders occupying names that public skills reference. That combination is rare enough to justify a human look every single time.
  3. Require attribution on defensive holds. This is the cheapest structural fix available. Make the good version distinguishable from the bad one by policy, because it is not distinguishable by inspection.

Automating the check

Checking that a name exists is one API call. Checking that the package at that name is the project the skill meant is harder, and that is the half that fails silently. Across a whole collection, on every update, it is a CI job: a release gate if you publish skills, an ingestion gate if you host them, a recurring check if you install them. We built SkillTracer for this, and it produced the findings discussed here.

7.  Nothing here is broken

The skill author wrote install instructions. The tool authors published on GitHub instead of PyPI, which is allowed and common. PyPI allocates names first come first served, which is how it has always worked and mostly works fine. The agent followed the setup steps in a document it was handed, which is the entire function of an agent. Every participant behaved reasonably, and the sum of all that reasonable behaviour is a remote code execution path onto the workstation of every security researcher who runs this skill, available to anyone with a free account and no particular skill.

We, as a community, have spent years hardening the artifacts in the supply chain. Signing, SBOMs, dependency scanning, version pinning, provenance attestation. All of it assumes the dependency exists. Every one of those controls operates downstream of the failure described here, because you cannot sign, scan, pin or attest a package that nobody has written yet. The vulnerability is in the sentence, not in the software.

Agent skills are executable documents. We ship them with the informality of a README and the blast radius of a build script.

And we hand them to something that reads instructions more literally, and far less suspiciously, than any human ever has.

Go and check your install lines. It is one API call per name, and right now that call is the entire control.

Annex.  Verification

Every status in this article was checked at the time of writing.

Apart from the above, there are more package names that we discovered, registered, and now hold, to disallow anyone with a malicious intent to perform a supply chain attack via those. As mentioned earlier, we have informed the upstream tool authors where one exists, and we will unconditionally transfer the ownership on request.

Checkout the SkillTracer here https://labs.metano.ai/scanner