Installation became a route to the next publication
The September 2025 Shai-Hulud campaign added a propagation loop to familiar package-credential theft. GitHub's incident account described compromised maintainer accounts and malicious post-install scripts. Wiz and other researchers analysed code that searched for secrets and used available npm credentials to publish additional malicious package releases.
The distinction is operationally important. A developer machine or CI runner consuming a dependency could also hold authority to distribute software. That made it a bridge from one compromised package to other packages, rather than merely an endpoint for data theft.
The first September campaign and later variants should not be collapsed into one set of payload details or counts. Reports were updated at different times and used different measures, including package names, versions and exposed repositories. A response should identify the wave and evidence it is actually addressing.
Draw a loop instead of a dependency tree
A normal dependency tree asks which packages an application consumes. This incident requires another edge: which packages the consuming environment can publish. The same machine can sit on both sides of the software-distribution relationship.
For each development and release environment, record whether it can access publishing credentials, which packages those credentials cover and when they become available. A repository may need to install dependencies during tests while having no legitimate reason to hold a publication credential at that stage.
Separating those stages can remove part of the propagation route. It does not establish that all install-time code is harmless, nor does it eliminate the need to review the credentials and network authority available to the job.
A recovery record for package maintainers
Begin with the package versions and executions supported by the current incident indicators. Establish whether the malicious lifecycle script ran and what authority was available in that environment. An affected dependency in a lockfile is evidence to investigate, but it is not the same as proof that a particular job executed it.
Then follow reusable credentials: package publishing, source control, cloud access and relevant service identities. Revoke or rotate according to the actual scope and the platform's current guidance. Inspect package publication and repository activity during the period in which those credentials could have been used.
If the environment had publication rights, examine what it could have released, not only what it installed. A cleanup that restores the original dependency while leaving a malicious release in another owned package has missed the loop that makes this campaign distinctive.
Lockfiles and provenance answer narrower questions
A lockfile helps identify resolved dependency versions. It does not establish that the selected version was benign, and a deterministic install can deterministically execute malicious lifecycle code. Preserve the exact package artefacts and the execution context needed to understand the finding.
GitHub's response described changes intended to strengthen npm authentication and publication. Those platform changes should be evaluated against the organisation's current workflow. A release using short-lived, narrowly scoped authority has a different recovery profile from a long-lived token copied across developer machines, but it still needs trustworthy workflow inputs and permissions.
The tj-actions compromise provides a useful earlier comparison: action code exposed CI secrets, leaving a credential-recovery task beyond the workflow fix. Shai-Hulud adds automated use of publishing authority to extend the incident into new consumers.
A release-pipeline exercise
Inspect lifecycle declarations without installing packages
In an existing local dependency tree that the team is authorised to inspect, this text search finds lifecycle declarations in package manifests:
rg --no-ignore -n --glob 'package.json' \
'"(preinstall|install|postinstall)"' node_modules
The command reads files; it does not run npm or execute the scripts. It is useful for scoping review in an already collected workspace. It does not inspect packages that are absent from that workspace, and the result may include ordinary legitimate build or setup tasks.
Do not install a suspected package merely to obtain this listing. Preserve the relevant package artefact and inspect it through the incident-analysis process. The fact that a package declares a lifecycle script does not identify malware; the script's content, provenance and actual execution context matter.
The complementary check is credential availability during installation. Review the job configuration and secret-injection boundaries without printing secret values. Record which credential classes become available, their scope and the phase in which they are introduced. A masked CI log does not demonstrate that an executed process could not read the underlying value.
Examine what the environment could publish
For every publication credential plausibly exposed, identify its package scope and inspect the corresponding release history over the incident window. Separate a suspicious new publication from a confirmed malicious artefact and preserve the package identity supporting each conclusion.
Coordinate revocation with maintainers and downstream users through established channels. Do not silently replace an affected release while leaving consumers without a version boundary or remediation statement. A clear affected-artefact notice is part of containment in a distribution incident.
This extends the response from the consumed dependency to the packages and repositories that available authority could alter, which is the defining loop in this campaign.
Use a synthetic dependency and a test publishing destination. The test should establish whether the install phase can see publication credentials, whether a build can alter its own release policy and which audit records would reveal an unexpected publication. No malicious package or live registry takeover is required.
Document each boundary crossed and the control enforcing it. “We have a separate release job” is only a starting description if the earlier job can read its secrets or change its definition. The useful outcome is a release path in which consuming untrusted code does not silently grant the power to distribute the next compromised version.
