Safeguard
Incident Analysis•

@acitons/artifact: The npm Typosquat Built to Steal GitHub Actions Tokens

A year ago, @acitons/artifact copied GitHub's own @actions/artifact and ran a postinstall binary that hunted for CI tokens. What it did, what the 206,000 figure really measured, how to hunt for it, and why 2026's signed malicious releases make the lesson sharper.

Safeguard Security Team
35 min read

@acitons/artifact was a malicious npm package that copied the name of @actions/artifact, the official GitHub Actions toolkit library for uploading and downloading workflow artifacts, with two letters swapped. Researchers at Veracode identified it on Friday 7 November 2025. Six of its versions, 4.0.12 through 4.0.17, carried a postinstall script that downloaded a compiled binary, ran it, checked whether it was inside a GitHub Actions job for a repository owned by the GitHub organization, and if so encrypted the build environment and sent it out. In Veracode's analysis, the goal was to "exfiltrate the tokens available to the build environment" and "use those tokens to publish new malicious artifacts as GitHub."

GitHub later told the researchers that the packages "were part of a tightly controlled exercise conducted by GitHub's Red Team" and that "at no point were GitHub systems or data at risk." npm replaced the package with a security holding package on 12 November 2025, and GitHub published malware advisory GHSA-j4mh-rqg3-26wp the same day.

Why write about it a year later? First, the "206,000 downloads" figure that travels with this story describes the real package, not the fake one. Second, the attack shape this exercise rehearsed, a package that runs inside CI and reaches for the pipeline's identity, became the defining supply chain pattern of 2026. Mini Shai-Hulud, the AsyncAPI compromise and GHAPPIER all ended with malicious npm releases that carried valid provenance because the attacker had reached the project's own CI identity. If your threat model still treats a signed package as a safe package, this post is for you.

The short version

ItemDetail
Malicious package@acitons/artifact (npm)
Imitated package@actions/artifact, part of GitHub's actions/toolkit
Malicious versions4.0.12 to 4.0.17 per the original research; the OSV record lists 4.0.12, 4.0.13, 4.0.15, 4.0.16 and 4.0.17; the GitHub advisory covers all versions
First version of the fake4.0.0, published 29 October 2025, 00:12 UTC
Identified7 November 2025, by Veracode's threat research team
Triggerpostinstall lifecycle script during npm install or npm ci
TargetGitHub Actions jobs where GITHUB_REPOSITORY_OWNER was github
RemovedBad versions pulled; npm swapped in 0.0.1-security on 12 November 2025
AdvisoriesGHSA-j4mh-rqg3-26wp (malware, CWE-506), OSV MAL-2025-49393
GitHub's statementA controlled GitHub Red Team exercise; no GitHub systems or data at risk
The 206,000 figureWeekly downloads of the real @actions/artifact (205,496 from 1 to 7 November 2025 per the npm API), not of the typosquat

What @actions/artifact is, and why a copy of it is dangerous

@actions/artifact lives in the actions/toolkit monorepo next to @actions/core, @actions/github and the other libraries that JavaScript actions are built on. It is the client that actions/upload-artifact and actions/download-artifact use to talk to GitHub's artifact service. In the toolkit's own source, the library reads ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL from the environment, because those are the credentials and endpoint the runner hands to actions so they can store artifacts. You can see this in config.ts.

That tells you who installs it: people writing custom actions, internal CI tooling and release scripts. Their code runs on runners, next to tokens. A typo in that dependency lands in the job that builds and ships your software.

Popularity helped too. The npm downloads API shows 205,496 downloads of the real package between 1 and 7 November 2025, and 1,105,336 for 6 to 12 November 2026. A name with that much traffic gets typed by hand, pasted from READMEs and written by coding assistants.

Timeline

All times are UTC and come from the npm registry metadata for the package, the advisories and the published research.

Date and timeEvent
24 Oct 2025, 17:44GitHub publishes the real @actions/artifact 4.0.0
29 Oct 2025, 00:12@acitons/artifact 4.0.0 appears, matching the real major version
31 Oct 2025, 01:52 to 02:09Versions 4.0.1 to 4.0.11 published in about 17 minutes
5 Nov 2025, 21:294.0.12, the first version the research flags as malicious
5 Nov 2025, 22:184.0.13, the version analysed in detail
6 Nov 2025, 17:04 and 19:494.0.14 and 4.0.15
6 Nov 2025 (end of day)The analysed "harness" binary is set to stop running after this date
7 Nov 2025, 14:44 and 15:424.0.16 and 4.0.17
7 Nov 2025Veracode identifies the package
9 Nov 2025, 00:17OSV publishes MAL-2025-49393, crediting Amazon Inspector as its source
10 Nov 2025Veracode publishes its research; the latest version is by then a clean 4.0.10
11 Nov 2025The Hacker News reports GitHub's Red Team statement
12 Nov 2025, 03:24npm publishes 0.0.1-security; GHSA-j4mh-rqg3-26wp published

Two details matter for defenders. The fake shipped eleven harmless versions first, which builds history and makes a package look less new. And the malicious window was short, roughly 5 to 7 November, with the binary coded to expire. Short windows are why "we would have noticed" is a weak control.

Technical details: how the payload worked

The description below comes from the original research and the advisories. We have left out anything that would help someone rebuild the tool.

Stage 1: the postinstall hook

The malicious versions added a postinstall entry to package.json. In November 2025 the current npm release line was 11, which runs the lifecycle scripts of dependencies by default during npm install and npm ci, so simply resolving the package was enough. The script used curl to fetch a file named harness from a GitHub Gist owned by the account jmasdg, saved it as ci_test_harness, marked it executable and ran it.

Stage 2: the harness binary

The binary was "an obfuscated shell script that had been compiled using the Shell Script Compiler tool" (shc), which wraps a script in an ELF executable. The research says it "was not detected by any popular anti-virus products" when it was analysed. It carried a kill switch: the "harness" sample refused to run after 6 November 2025 UTC, and a second sample, named "tester", was set to expire a day later. When it ran, it set an environment variable, re-executed itself under bash and unpacked a small Node.js package containing an obfuscated verify.js.

Stage 3: verify.js and the targeting gate

verify.js checked for the GITHUB_ environment variables that GitHub Actions sets in every job. Then it compared the repository owner against a target list containing one entry, github, and exited if there was no match. Outside GitHub's own repositories, it did nothing further. Inside them, it collected the environment, fetched an AES key from a dynamic DNS hostname, encrypted the data into a file named env.enc and uploaded it to a URL on app.github.dev. That domain is used for Codespaces forwarded ports, so the exfiltration endpoint sat on GitHub-owned infrastructure. Traffic to it would look ordinary to many egress filters.

Indicators of compromise

These are from the original research and the advisories, defanged where they are network locations.

TypeValue
npm package@acitons/artifact 4.0.12 to 4.0.17
Related npm package8jfiesaf83 1.0.0 to 1.0.11
npm publisherblakesdev
GitHub accountsjmasdg, f8snaf, s0larized
Dropped fileci_test_harness
Staging fileenv.enc
Key serverhxxps://83hfhjasksn.hopto[.]org
Exfiltration hostlaughing-space-capybara-x5g6rjxq7jwvfp6q6-443.app.github[.]dev
SHA-256e3a6d0d139dc56f28f82ec161b3d17ecd137b088acd3a0e8330a5d412c025b73
AdvisoriesGHSA-j4mh-rqg3-26wp, MAL-2025-49393

The advisory's guidance is blunt

GitHub's malware advisory says: "Any computer that has this package installed or running should be considered fully compromised. All secrets and keys stored on that computer should be rotated immediately from a different computer." That applies even though the payload was gated on the repository owner. A runner or laptop that installed a malicious version downloaded and executed an unknown binary. The gate in verify.js describes what the analysed sample chose to do. It does not prove what every sample did.

What the 206,000 figure really measured

Much of the coverage carried a headline about a malicious package "with 206k downloads". The original research says the real @actions/artifact "has accumulated over 206k downloads", and the npm downloads API confirms 205,496 downloads of the real package for 1 to 7 November 2025. That is a weekly number for the legitimate library.

For the typosquat itself, The Hacker News reported 31,398 weekly downloads and, citing npm-stat, 47,405 total downloads. We cannot reproduce those numbers today. The npm downloads API now returns zero for @acitons/artifact across late October and early November 2025, which is consistent with the package's history being reset when npm replaced it. Treat both figures as reported, not confirmed.

Even reported figures overstate victims, because download counts include mirrors, scanners that pull every new package and repeated CI runs. The right question is not how many times it was downloaded worldwide. It is whether any lockfile, cache or runner of yours ever resolved it.

A red team exercise. Why it still matters

GitHub's statement is specific, and nothing in the public record contradicts it. Defenders should still take two lessons from it. First, the technique worked on the public registry: a typosquat, a postinstall hook and a CI-aware payload passed npm's publish checks, built up history and were caught by outside researchers. Anyone could publish the same shape tomorrow against a library your actions depend on. Second, the payload aimed at identity, not data. Taking the build's tokens to publish as the victim is the objective every serious CI attack of 2026 pursued. And anyone who installed the fake package, by mistake or out of curiosity, still ran an unknown binary, which is why the advisory says to treat those hosts as compromised.

Typosquatting in CI: why the build runner is the target

Typosquatting is old. What changed is where the typo executes. On a developer laptop, a bad package meets an endpoint agent, a user session and a short list of personal credentials. On a CI runner it meets the keys to the release process.

How a misspelled scope reaches a pipeline

  • Hand-written internal actions. Custom JavaScript actions depend on @actions/core, @actions/github and @actions/artifact, often added by someone who writes one action a year.
  • Copied snippets and coding assistants. README examples get pasted and assistants write dependency lists from memory. A plausible wrong scope slips through because readers check the part after the slash.
  • Transitive pulls. A helper package you trust can add the misspelled dependency in a patch release. Your manifest never changes, only your lockfile.

Why lifecycle scripts made CI the perfect place to detonate

On npm 10 and 11, the ignore-scripts setting defaults to false, so every preinstall, install and postinstall script in the dependency tree runs with the permissions of the job, before a single test executes. On a CI runner that means the script inherits the job's environment variables, its working directory, the Git credentials the checkout step configured, any cloud session established earlier in the job and network egress to wherever the runner can reach. npm also runs dependency scripts quietly: unless foreground-scripts is on, their output does not show up in the job log.

That default has since changed. GitHub's npm v12 changelog says npm install "will no longer execute preinstall, install, or postinstall scripts from dependencies" unless the project approves them, with the allowlist "written to package.json". npm 12.0.0 reached the registry on 8 July 2026. Plenty of CI images and pinned toolchains still run npm 10 or 11, though, so check what your runners actually use before you count on the new default. Our post on how attackers adapted after npm 12 disabled install scripts covers what came next.

Version mimicry and timing

The fake started at 4.0.0 five days after the real package released 4.0.0, so it looked current at a glance. Clean versions gave it history; the malicious ones arrived in a short window and expired. Put it in review checklists: a dependency whose first release is days old, and whose version mirrors a better known package, deserves a second look.

Why CI secrets and OIDC tokens are the prize

A CI job is a short-lived machine that briefly holds more authority than most humans in the company. Here is what a malicious install script can typically reach on a GitHub-hosted runner.

The GITHUB_TOKEN and checkout credentials

Every job receives a GITHUB_TOKEN scoped to the repository, with permissions set by the workflow's permissions block or the organization default. actions/checkout persists credentials by default so later steps can run authenticated Git commands; its README says "the auth token is persisted in the local git config" and tells you to "set persist-credentials: false to opt-out". From checkout v6 the credential lives in a separate file under $RUNNER_TEMP rather than in .git/config. Either way it sits on the same disk as your node_modules. With write permissions, that token can push commits, open pull requests and create releases.

The runtime token

Actions get ACTIONS_RUNTIME_TOKEN so they can talk to the artifact and cache services. That is the token @actions/artifact reads. Write access to shared storage such as the cache is a path to poisoning what later jobs restore, which is how the TanStack compromise in May 2026 moved from a pull request into a release.

The OIDC token

When a job has permissions: id-token: write, the runner can mint a JSON Web Token that proves the workflow's identity. GitHub's OIDC reference says this setting "only enables fetching and setting the OIDC token; it does not grant write access to other resources." That is accurate, and easy to over-read, because the token is accepted by everything you have configured to trust it. The runner exposes ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN so code in the job can request one. Cloud providers exchange it for short-lived cloud credentials when its claims match a trust policy. npm's trusted publishing exchanges it for "short-lived, scoped credentials", and for public packages published from a public repository, "npm automatically generates and publishes provenance attestations".

The OIDC token is the release identity. Short-lived credentials remove the stored-secret problem, but not malicious code running inside the trusted job and asking for a token itself. See our explainer on whether a CI OIDC token is a secret.

Everything else in the environment

Then there is the usual list: NPM_TOKEN or NODE_AUTH_TOKEN, cloud keys stored as repository secrets, signing keys and deploy keys. Secrets passed with env: at the job level are visible to every step, including the install step. GitHub's secure use reference asks that workflow credentials "have the least privileges required". A token that can publish a package, push to a release branch or assume a cloud role turns one bad install into a compromise of everyone downstream.

The 2026 pattern: malicious releases with valid provenance

The @acitons/artifact exercise rehearsed reaching a build's identity through a bad dependency. During 2026, attackers repeatedly reached that identity for real, through the pipeline rather than the registry account. The result was a new kind of malicious release: one that carried genuine, verifiable provenance. There is no public evidence of any operational link between the exercise and the incidents below. The connection is the technique.

Mini Shai-Hulud and TanStack, May 2026

TanStack's postmortem is one of the clearest public accounts of a CI identity compromise. Between 19:20 and 19:26 UTC on 11 May 2026, an attacker published 84 malicious versions across 42 @tanstack/* packages. The chain had three links. A pull_request_target workflow built code from a fork, the "Pwn Request" pattern. That code wrote a poisoned pnpm store into the GitHub Actions cache under a key the release workflow would later restore. When the release workflow ran on main, attacker code dumped the runner worker's memory, extracted an OIDC token and published directly, "bypassing the workflow's Publish Packages step entirely". TanStack states: "No npm tokens were stolen and the npm publish workflow itself was not compromised."

Public detection came within 20 to 26 minutes, and TanStack's response table shows all 84 versions deprecated about 1 hour 43 minutes after publish. The Cloud Security Alliance's research note of 15 May 2026 called it the "first documented npm worm to produce legitimately attested malicious packages," and reports more than 170 npm and PyPI packages and over 400 malicious versions across the wider wave. Those totals are the note's, not a registry statement. Our TanStack coverage has the package detail.

AsyncAPI, July 2026

AsyncAPI's own security issue records that on 14 July 2026 at 07:10 UTC, @asyncapi/generator 3.3.1, @asyncapi/generator-helpers 1.1.1 and @asyncapi/generator-components 0.7.1 were published "via a compromised CI workflow". A commit pushed to the next branch at 06:58:42 UTC, authored by the placeholder identity "Your Name", injected an obfuscated payload into three source files. The project's bot then published all three packages "via GitHub Actions OIDC within seconds." The maintainers were explicit that the project and its maintainers "are victims of this attack".

Two details stand out. The payload "runs immediately when the poisoned file is require()d", with no install hook at all, so blocking install scripts would not have stopped it. And because the project's real pipeline published the code, the releases carried valid provenance. How the attacker first obtained write access is still unconfirmed in the issue thread, and security vendors' accounts differ on it and on a possible fourth package; treat those points as reported. See our AsyncAPI analysis for the full chain.

GHAPPIER, September 2026

CloudSEK's report, dated 20 September 2026, says that on 9 September 2026 someone used the maintainer account of @dforge-core/dforge-mcp for 105 minutes. The attacker "changed three lines so that any push to the main branch started the release workflow," then rewrote the workflow to publish unattended. Version 0.2.21 "was built by GitHub Actions through OIDC trusted publishing" and carried valid npm provenance, with its attestation in a public transparency log naming the attacker's own commit. It stayed the latest release "for 35 minutes and 38 seconds" before the maintainer published a clean 0.2.22. The researchers say the wider operation spans at least 65 public repositories. Their summary is worth keeping: "provenance attests where an artefact was built, not whether its source was honest."

What provenance proves, in npm's own words

npm's documentation on provenance statements is plain: "When a package in the npm registry has established provenance, it does not guarantee the package has no malicious code." Provenance tells you which repository, commit and workflow produced a tarball, which lets you notice a publish from an unexpected workflow, branch or commit. But a valid attestation from a hijacked pipeline is still valid. More in what package provenance proves and what it does not.

CaseEntry pointIdentity abusedProvenance on the bad release
@acitons/artifact (Nov 2025, exercise)Typosquatted dependency with postinstallBuild tokens in the job environment (the stated aim)Not applicable; a separate fake package
TanStack (May 2026)pull_request_target plus cache poisoningOIDC token read from runner memoryValid
AsyncAPI (Jul 2026)Write access to a release branchProject bot publishing via OIDCValid
GHAPPIER (Sep 2026)Maintainer account, workflow rewrittenTrusted publishing on push to mainValid

How to hunt for it in lockfiles and workflow logs

Start with the specific package, then hunt the general pattern. The goal is a written answer: "we never resolved it" with evidence, or a list of hosts and runs to investigate.

1. Search every lockfile you have

A lockfile records what was actually resolved. Search all of them, including those in action repositories, internal tooling repositories and archived branches.

# In a local checkout or a mirror of many repositories
grep -rIl \
  --include=package-lock.json --include=npm-shrinkwrap.json \
  --include=yarn.lock --include=pnpm-lock.yaml --include=bun.lock \
  -e '@acitons/' -e '8jfiesaf83' .

For an organization on GitHub, code search covers default branches quickly:

gh search code '"@acitons/"' --owner YOUR_ORG --limit 100
gh search code '"8jfiesaf83"' --owner YOUR_ORG --limit 100

Code search does not cover every branch, fork or deleted file. Use it for a first answer, then confirm with history.

2. Search history, not just the current tree

A dependency that was added and removed inside a week will not be in today's lockfile. Git keeps it.

git log --all -p -S '@acitons/' -- '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' '*package.json'

Run it in every repository that builds JavaScript, and in any repository that holds a custom action.

3. Check caches and mirrors

Search your registry proxy's storage and request logs for both names. A stale node_modules or package-manager store in the GitHub Actions cache can outlive the lockfile change that introduced it; list caches with gh cache list and remove suspect entries with gh cache delete.

4. Search workflow logs and runner telemetry

Logs from November 2025 are probably past retention. Where logs exist, search for the package name and payload artifacts:

gh run list --repo YOUR_ORG/YOUR_REPO --limit 200 --json databaseId,createdAt,displayTitle
gh run view RUN_ID --repo YOUR_ORG/YOUR_REPO --log \
  | grep -nE 'acitons|8jfiesaf83|ci_test_harness|env\.enc|hopto\.org|app\.github\.dev'

Because npm hides dependency script output by default, a clean log is weak evidence. Network logs are stronger. If self-hosted runner egress goes through a proxy, search it for gist.githubusercontent.com requests from install steps, for hopto.org and for unexpected *.app.github.dev hosts. Those last two are unusual destinations for a build.

5. Review what the token could have done

For any hit, check the job's permissions block, whether id-token: write was granted, which secrets were mapped in and which cloud roles trust that repository's OIDC subject. Then read the organization audit log and cloud logs for activity by those identities around the run time, and rotate from a clean machine.

6. Hunt for the pattern, not just this name

The next typosquat will have a different name. Two cheap checks catch the shape.

First, list every npm scope your lockfiles resolve and compare them with the scopes you expect. Most organizations depend on a few dozen scopes, so an unfamiliar one stands out. A small script can flag scopes one or two edits away from a scope you trust:

import json, sys

TRUSTED = {"@actions", "@octokit", "@types", "@babel", "@aws-sdk"}

def dist(a, b):
    prev = list(range(len(b) + 1))
    for i, ca in enumerate(a, 1):
        cur = [i]
        for j, cb in enumerate(b, 1):
            cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (ca != cb)))
        prev = cur
    return prev[-1]

lock = json.load(open(sys.argv[1]))
scopes = {p.split("node_modules/")[-1].split("/")[0]
          for p in lock.get("packages", {}) if "node_modules/@" in p}
for s in sorted(scopes - TRUSTED):
    near = [t for t in TRUSTED if dist(s, t) <= 2]
    if near:
        print(f"{s} looks like {near}")

Second, list dependencies that have install scripts. npm records hasInstallScript in package-lock.json versions 2 and 3, so a new entry with an install script is easy to flag for a reviewer. Our guide to what to check in a lockfile diff covers the review side.

Hardening CI against the next one

None of these steps needs a vendor. They cut the chance that a bad package runs, and the damage if it does.

Stop install scripts from running by default

On npm 12, keep the new default and approve only the packages that truly need a build step with npm approve-scripts, then commit the allowlist. On npm 10 or 11, set ignore-scripts=true in the CI .npmrc. pnpm's build settings say packages not listed in allowBuilds "are disallowed by default and are treated as unreviewed", and with strictDepBuilds (default true) "the installation will exit with a non-zero exit code if any dependencies have unreviewed build scripts." Remember the AsyncAPI lesson: blocking install scripts stops postinstall payloads, not code that runs when it is imported.

Add a release-age cooldown

Most malicious versions are found within hours or days. npm's min-release-age setting, off by default, installs "only versions that were available more than the given number of days ago". pnpm's minimumReleaseAge is set in minutes and documents a default of 1440. Note the unit difference if a monorepo uses both tools. A seven-day cooldown would have kept every malicious @acitons/artifact version out of builds, because all of them were gone within a week of publication. See our guide to cooldown policies for new package versions.

Install from the lockfile only

Use npm ci, pnpm install --frozen-lockfile or yarn install --immutable in CI, so a job can never resolve a version nobody reviewed. Our comparison of frozen-lockfile flags covers the differences.

Shrink what a job can reach

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@SHA_OF_A_REVIEWED_RELEASE
        with:
          persist-credentials: false
      - run: npm ci --ignore-scripts
      - run: npm test

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@SHA_OF_A_REVIEWED_RELEASE
        with:
          persist-credentials: false
      - run: npm ci --ignore-scripts
      - run: npm publish

The pattern: read-only GITHUB_TOKEN by default, persist-credentials: false unless a step really pushes, and id-token: write only on the publish job, behind a protected environment with required reviewers. Our guide to GITHUB_TOKEN least privilege goes further.

Separate untrusted and trusted workflows

GitHub's secure use reference says to "avoid using the pull_request_target workflow trigger if it's not necessary", that such workflows "must not explicitly check out untrusted code", and that these triggers "share the same cache of the main branch with other privileged workflow triggers". That is the TanStack chain in one sentence. Do not restore caches written by pull request workflows in release workflows. See our explainer on pull_request_target and fork safety.

Lock the workflow graph

GitHub made workflow dependency lockfiles generally available on 28 October 2026. The gh actions-lock extension writes .github/workflows/actions.lock, recording exact commits for actions and their transitive dependencies, and a Require lockfile policy enforces adoption. The secure use reference calls this "the strongest protection available" and says it "supersedes SHA pinning for repositories that adopt it." Locking does not decide whether a dependency is safe, so review before you lock. Elsewhere, pin actions to full commit SHAs.

Lock down publishing

Move to npm trusted publishing, then select "Require two-factor authentication and disallow tokens" so a stolen long-lived token cannot publish. Bind the publisher to one workflow file and environment, and publish only from a protected branch or tag. A release whose attestation names an unexpected workflow, branch or commit is an alarm, even though the signature verifies.

Control egress

A build job rarely needs the whole internet. Restricting runner egress to your registry, source host and test services makes a payload's download and upload steps fail loudly. Watch destinations that blend in, such as Gist raw URLs and forwarded Codespaces ports; this payload used both.

What it means for the business

For a security leader, the cost of an incident like this is mostly response. Even a gated payload forces the same work: find every place the package resolved, decide which hosts are compromised, rotate what they could reach and prove to customers and auditors that you did. Teams without a dependency inventory spend days on the first step. Teams with one spend minutes.

The publish identity raises the stakes. If your CI publishes packages, images or releases that customers install, a pipeline compromise becomes your customers' incident, and their security questionnaires will ask about it.

The controls map to frameworks you already report against. SOC 2's CC6.8 covers unauthorized or malicious software and CC8.1 covers change management. ISO/IEC 27001:2022 Annex A 5.21 covers the ICT supply chain and 8.32 covers change management. A committed lockfile, a dependency inventory, a cooldown policy and least-privilege workflows produce evidence for all of them.

Three questions are worth putting to your own teams this quarter:

  1. Can we list, within an hour, every repository and runner that resolved a given npm package in the last 90 days?
  2. Which of our jobs hold a publish or deploy identity, and do dependencies install inside those jobs?
  3. If a release of ours carried a valid signature but came from an unexpected workflow run, who would notice, and how fast?

Where Safeguard fits

Safeguard did not identify @acitons/artifact. Veracode's researchers did, and GitHub, npm and OSV handled the advisories and the takedown. What Safeguard offers is the inventory, the enforcement and the evidence that make the response to the next one fast.

Know every place a package resolved. Safeguard's SCA builds a full transitive dependency inventory from npm, Yarn and pnpm lockfiles across your repositories, and generates and monitors SBOMs in SPDX and CycloneDX. When an advisory names a package, "where do we use it" becomes a search instead of a project. Asset discovery finds the repositories, images, packages and SBOMs you may not have registered, including the small internal action repositories where a dependency like @actions/artifact usually lives.

Stop bad packages at install time. Safeguard Package Firewall runs as an install-time proxy for npm and pip on developer machines and CI runners, and blocks malicious, typosquatted and dependency-confusion packages before they install. Because it sits in front of the install, it applies before any lifecycle script in the package can run.

Turn policy into gates. Safeguard policies can be written in plain English and enforced as gates at build, CI and release. The Safeguard CLI runs inside GitHub Actions, GitLab CI, Jenkins and Azure DevOps, so the same rules apply to pull requests and release jobs. Our walkthrough on adding Safeguard to GitHub Actions shows a working setup with SBOM generation and a policy gate.

Hold suppliers to the same standard. Safeguard TPRM lets you onboard vendors, request SBOMs, and score and monitor supplier risk, so when a supplier's pipeline is in the news you can track what you asked and what they answered.

Produce the evidence. Safeguard's compliance automation maps controls, evidence and vendors across SOC 2, ISO 27001, GDPR and other frameworks, so the inventory export, policy configuration and rotation record land against the controls they satisfy. Safeguard itself is SOC 2 Type I and Type II audited.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Griffin, Safeguard's security model, opens a policy gated pull request that moves the affected dependency to the safe version, and your own CI runs your tests on it before anyone merges. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Auto-merge is a separate switch and stays off by default, so your team decides what merges on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Safeguard Gold rebuilds patched versions of open-source packages and images from source, signs each build, attaches an SBOM, SLSA provenance and VEX, and serves them from registry.safeguard.sh to your organization. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened build of a specific package.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

For a typosquat in your CI, today

A malicious package has no fixed version to upgrade to. The fix is to remove it, contain what it touched and make the next one harder. None of this waits on anyone else.

  1. Search lockfiles, history, caches and code for @acitons/ and 8jfiesaf83, and record the result even when empty.
  2. Replace any hit with the real @actions/artifact, regenerate the lockfile and purge related Actions caches.
  3. Rotate every secret a host that installed a malicious version could reach, from a different machine.
  4. Review the job's permissions, OIDC trust policies and recent publishes by those identities.
  5. Block install scripts in CI with npm 12, ignore-scripts=true or pnpm's allowBuilds.
  6. Add a cooldown with min-release-age or minimumReleaseAge.
  7. Split build from publish so id-token: write never sits in a job that installs dependencies.
  8. Alert when one of your releases is attested by an unexpected workflow, branch or commit.

FAQ

What was @acitons/artifact?

It was a typosquat of GitHub's @actions/artifact npm package, with "ti" swapped to "it" in the scope. Versions 4.0.12 to 4.0.17, published 5 to 7 November 2025, ran a postinstall script that downloaded and executed a binary which, inside GitHub Actions jobs for repositories owned by the GitHub organization, encrypted and exfiltrated the build environment. Veracode identified it on 7 November and npm replaced it on 12 November 2025.

Was it a real attack or a test?

GitHub said the packages "were part of a tightly controlled exercise conducted by GitHub's Red Team" and that "at no point were GitHub systems or data at risk." The technique was real and worked on the public registry, so it remains a valid model of a CI attack.

Did @acitons/artifact really have 206,000 downloads?

No. The 206,000 figure is the weekly download count of the legitimate @actions/artifact, confirmed at 205,496 for 1 to 7 November 2025 by the npm downloads API. A news report cited 47,405 total downloads of the typosquat from npm-stat, which we could not reproduce after the package was replaced.

I installed it. What should I do?

Treat that machine as compromised, as GitHub's advisory GHSA-j4mh-rqg3-26wp says. Rotate every secret and key it could reach from a different computer, remove the package, rebuild the runner or workstation from a clean image and review recent activity by any token that was available to the job.

Does npm provenance mean a package is safe?

No. npm's documentation says provenance "does not guarantee the package has no malicious code." It shows where and how a package was built. In 2026, the TanStack, AsyncAPI and GHAPPIER releases all carried valid provenance because the attackers used the projects' own pipelines.

Would --ignore-scripts or npm 12 have stopped it?

For @acitons/artifact, yes, because the payload ran from postinstall. Neither would have stopped the AsyncAPI payload, which ran when the code was imported. Combine script controls with a release-age cooldown, lockfile-only installs and least-privilege jobs.

Did Safeguard find this package?

No. Veracode's researchers identified it, and GitHub, npm and OSV published the advisories. Safeguard helps teams respond to events like this: SCA and SBOM inventory to find where a package resolved, Package Firewall at install time, policies and gates in CI and compliance evidence for the response.

See how Safeguard would have helped

These demos show the product, not this incident. Each is a flow you would use around a malicious package report.

The first demo shows the Safeguard CLI fixing a lodash CVE: it reads the finding and the advisory, asks before any command that changes something, edits package.json, runs the install and the tests, and prints a summary without pushing. Swapping @acitons/artifact for the real @actions/artifact in a custom action repository follows the same review-first flow.

Click through it at your own pace, or open it full screen.

The second demo sets up the Safeguard runner on your own machine, approves it with its device code and then scans from the terminal. Here you would scan your internal action and tooling repositories, where a toolkit dependency is most likely to live.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude may use. During a response, your team can ask in the chat it already uses which repositories carry a named package. What it can answer depends on what your workspace has scanned.

Click through it at your own pace, or open it full screen.

Quick check

Your CI uses npm ci with a committed lockfile. Are you safe from a typosquat like this?

Not on its own. A lockfile pins what was resolved, so it stops a new version sneaking in, but if the misspelled package was added in a reviewed pull request, the lockfile pins the malicious version faithfully. Add install-script controls, a release-age cooldown and a scope review on dependency changes.

A new release of your own package verifies with npm audit signatures, but the attestation names a workflow run on a branch you do not release from. What do you do?

Treat it as a possible pipeline compromise. A valid signature proves the release came from your CI, not that the code was yours. Deprecate the version, check the commit and workflow history for that run, review who could push to that branch and rotate the publish identity's trust configuration.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

We email you a link to confirm, and only then add you to the list. Unsubscribe in one click. Privacy

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.