On October 5, 2026, two @subql/common releases went through npm's trusted publishing in 32 minutes: a canary, then 5.8.3, which Corgea found harvested credentials. Of the package's 288 versions, exactly one carries a provenance record, the canary's, and it names a branch called chore/ci-audit-55. What npm's binding checks, what provenance proves, and three lookups a consumer can run.
At 11:23:01 UTC on October 5, 2026, a branch named chore/ci-audit-55 appeared in the subquery/subql repository on GitHub. Two seconds later it held one commit, "chore: audit ci config", which deleted 290 lines of the project's publish.yml and added 12. The new workflow did very little. It downloaded the already-published @subql/common 5.8.2, renamed it 5.8.3-onf-rt1, gave it the description "RED TEAM VERIFICATION BUILD - harmless canary, ignore/unpublish", and published it to npm under a tag called redteam.
npm accepted it at 11:24:40. At 11:27 the branch was gone. At 11:52 a branch with the same name came back, carrying a commit called "[release] 5.8.3", and at 11:56 npm accepted @subql/common 5.8.3. That release swapped the package npm was about to publish for a tarball fetched from an outside host, ci-artifacts.dev. The security firm Corgea, which took it apart, found code that ran on install and again on import, and "harvested local, cloud, Kubernetes, Vault, and GitHub credentials."
Neither publish needed a stolen npm token. Both went through npm's trusted publishing, the route GitHub and npm had spent the previous year telling maintainers to adopt. The registry holds a provenance record for only one of them, the canary. It is the only provenance record @subql/common has, and it names the attacker's branch.
After the Shai-Hulud worm, GitHub laid out its plan for npm. "On September 14, 2025, we were notified of the Shai-Hulud attack, a self-replicating worm that infiltrated the npm ecosystem via compromised maintainer accounts," its post began. It set out to deal with "token abuse and self-replicating malware," and the route it pushed was trusted publishing, which "was pioneered by PyPI in April 2023": a CI workflow proves who it is to npm with a short-lived OpenID Connect token, so no long-lived secret sits in the build system to steal. "We strongly encourage projects to adopt trusted publishing as soon as possible."
npm did not oversell what comes with it. Its documentation describes provenance, the signed record of where a package was built, as something that "allows developers to verify where and how your package was built before they download it," and it is careful about the limit: "When a package in the npm registry has established provenance, it does not guarantee the package has no malicious code. Instead, npm provenance provides a verifiable link to the package's source code and build instructions, which developers can then audit and determine whether to trust it or not."
So the interesting question about October 5 is not whether the system lied. It didn't. It is what each check checked.
When a maintainer sets up trusted publishing for GitHub Actions, npm asks for five things: "Organization or user (required)," "Repository (required)," "Workflow filename (required)," "Environment name (optional)" and "Allowed actions (optional)." For the filename it adds: "Enter only the filename, not the full path." There is no field for a branch.
That matters because of how subql's workflow could be started. Its publish.yml on the main branch runs on a push to main, ignoring changes to the workflow files themselves, and on workflow_dispatch, GitHub's trigger for running a workflow by hand. A manual run can be pointed at any branch, and it runs the workflow file as that branch has it. Anyone able to push a branch and start a run could change what publish.yml did without touching main, and the run would still present npm with the right organization, repository and filename.
That is what the two commits did. The release commit even widened the publish job's guard to accept github.event_name == 'workflow_dispatch', and added a step named "Sync common artifacts from release mirror" that downloaded the replacement tarball. Corgea's summary is precise: the malicious 5.8.3 "was published through GitHub Actions OIDC as well," so the attacker had nothing to bypass. The feature was used as configured. Corgea also notes that "the downloaded archive had no hash or signature check," and that because the code ran on import as well as on install, "npm ci --ignore-scripts is not a complete answer."
Who did it is not in the record. The two commits carry a maintainer's name and are unsigned, and the security firm StepSecurity, which reconstructed the timeline, is careful on the point: "These records identify the account and metadata associated with the activity, not the person controlling the account or whether the activity was authorized." The canary looks like a test of the door before the real use of it. It went through, and thirty-two minutes later the real release went through the same way.
The reach was real. StepSecurity's scan of the @subql packages, using registry data captured at 12:39 UTC that day, found "19 latest package versions with a potential path to @subql/common@5.8.3," and at collection time "npm's latest tag pointed to 5.8.3."
The registry lists 288 versions of @subql/common, going back to December 2020. When I read them on October 27, exactly one carried a provenance attestation: the canary.
Its SLSA provenance, the standard format npm uses, says what it saw, and all of it is true:
.github/workflows/publish.yml;refs/heads/chore/ci-audit-55;workflow_dispatch;34128fd8;That run now answers 404 on GitHub. StepSecurity noted the record "preserves a now-unavailable Actions run identifier." The record is still there; the run it points to is not.
Why only one? The project's genuine trusted-published releases, 5.8.1 in November 2025 and 5.8.2 later that month, carry the GitHub Actions identity and the same trusted-publishing configuration as the canary, and no provenance at all. The difference is the tool. The canary's workflow ran npm publish, and npm's own command-line tool attaches provenance to trusted publishes automatically. The project's release action publishes with yarn npm publish --access public, and yarn makes provenance only when asked: its --provenance flag will "Generate provenance for the package," and it can also be switched on with a setting, an environment variable or a publishConfig field. The project used none of them.
So there was no history of green badges for the attack to blend into. The first provenance this package ever had appeared with the attack, and it said, in plain fields, that it came from a branch named chore/ci-audit-55 and was started by hand. What it lacked was anything to compare against.
What did 5.8.3 itself carry? This part is my inference. It went out through the same yarn step as the two clean releases, and nothing in the attacker's commit turned provenance on, so it probably carried none. The one gap is the swapped-in package, which brought its own modified package.json, and a publishConfig field there could have switched provenance on. The registry can't settle it. Its attestation URL now returns 404, and the version is gone from the registry's list.
ChainDrop, two months earlier, is the case a branch rule can't catch. Microsoft's security researchers, writing on August 4, described it as a "Mini Shai-Hulud variant" that reached "more than 400 packages across multiple unrelated publishers," among them packages in the keyv, flat-cache and cache-manager ecosystems. "Evidence points towards stolen maintainer credentials as the attack vector for initial compromise," they wrote, with later spread through stolen tokens and, in some workflows, GitHub Actions publishing. Of that route they said: "Packages published through this route can carry valid provenance because the publication originates from a legitimate workflow identity."
Microsoft also wrote that many malicious versions "had no corresponding source-code commit, pull request, tag, or legitimate release." Keyv was not one of those. Its maintainer's recovery note, posted on the project's issue tracker on August 5, lists what cleaning up took: "Disabled GitHub Actions and npm trusted publishing." "Worked with npm to remove the compromised versions from all affected Keyv packages and restore the correct distribution tags." "Deleted the malicious auxiliary branches and release tags." And the main branch had to be force-reset, past commits the attacker had put there.
That changes what a policy can do. A rule that accepts only builds from the default branch or a release tag would have rejected the subql canary, whose provenance names a throwaway branch. On keyv it promises nothing, because the attacker controlled the default branch and made tags, and a build from either would pass it. Every surviving provenance record for keyv, flat-cache and cache-manager, six in all, names either a release tag or refs/heads/main, and one of keyv's legitimate releases, 6.1.0, was itself a manual dispatch from main, so requiring the release event is not a workable rule either.
The difference shows up one lookup outside the attestation. GitHub still has the two malicious release commits, "release: v6.0.0" and "release: v6.0.1." Both are unsigned, and neither is attached to any pull request. The clean release commits beside them, "release: v6.0.0-rc.1 (#2039)" and the two betas before it, are each signed and verified and each came through one pull request. The thirty most recent commits on keyv's main branch today are all verified. Provenance tells you which commit a package was built from. Whether that commit was signed, and whether a pull request brought it in, are separate questions, and GitHub's API answers each in one lookup.
There is one more thing the records show. When npm removes a malicious version, its copy of the attestation goes with it. The attestation URLs for @subql/common 5.8.3, keyv 6.0.0, flat-cache 6.1.24 and cache-manager 7.2.10 all return 404 today.
That is the right call for users, who must never install those versions again. But npm's copy is not the only one. npm's documentation says a package published with provenance is "logged in a public transparency ledger," Sigstore's Rekor, and the canary's two records are there. Rekor files them under the tarball's SHA-512 digest. A search by that digest finds both; a search by the canary's commit hash, or by the SHA-1 checksum npm also lists, finds nothing. And when npm removed 5.8.3, the registry stopped listing its digest. StepSecurity, which downloaded the tarball, published its SHA-256. Rekor has nothing under that either, which settles nothing, because npm's records name the tarball by its SHA-512 only.
So if 5.8.3 or keyv 6.0.0 carried provenance, the record should still be in a public log, under a digest that only someone who kept the tarball or its old registry entry still has.
Provenance answers which workflow, which ref and which commit. It does not answer whether that commit was reviewed. Those are separate questions, and both have cheap answers.
If you consume packages, check three things before you trust a release that matters to you:
Each is a lookup against data that already exists, and none needs new tooling. The first catches chore/ci-audit-55. Only the third catches keyv's release commits.
If you maintain packages, the trusted-publishing form has one field that can pin a branch: the GitHub environment. Bind the trusted-publishing configuration to an environment and give that environment deployment-branch rules, and a workflow run from a throwaway branch can't publish. For more, npm offers stage-only publishing, under which "all CI-automated publishes go through the staged publishing flow, requiring a maintainer to review and approve each package with 2FA." Configurations created after September 3, 2026 allow staging by default. Those created before May 20, 2026, like this project's, were set to direct publishing only, and npm changed nothing for them. And if you publish with yarn, turn provenance on, so that a surprise has a baseline to be compared against.
The canary is still on the registry. Its description still asks to be ignored or unpublished, and npm view @subql/common dist-tags still lists a tag called redteam, pointing at the one version of the package that carries provenance.
--provenance option.Chain of Consciousness keeps a tamper-evident, dated record of what an agent or a pipeline did, in hash-linked entries, so a release, a changed workflow file or a published artifact can be read back later, in order, by anyone you show it to.
pip install chain-of-consciousness npm install chain-of-consciousness
Or start without installing anything: Hosted Chain of Consciousness.