TL;DR
TeamPCP and later TeamPCP-derived activity did not just compromise packages; they compromised trust. Across CI pipelines, registries, developer tools, and AI coding agents, legitimate automation became a path to credentials and the next victim.
Defenders should limit what builds can access, use short-lived identities, pin executable dependencies, delay new releases, review IDE and agent configuration like code, restrict runner egress, and treat credential rotation as full incident containment.
The seven lessons below turn these ideas into detailed, practical recommendations.
Arrests make for a neat ending. Software incidents rarely do.
Last week, two alleged members of TeamPCP were arrested in Australia. The arrests may mark the end of a six-month run of attacks that moved through security tools, package registries, GitHub Actions, IDE extensions, and eventually AI coding agents.
But the most useful part of the TeamPCP story is not who was arrested. It is how often the same basic failure repeated across very different companies.
We mapped 37 incidents across the campaign. From a distance, the chain looks chaotic: Trivy, npm, PyPI, Checkmarx, Bitwarden, TanStack, Nx, GitHub, Microsoft, and more. Up close, the pattern is remarkably consistent:
- Compromise something developers or build systems already trust.
- Let normal automation execute the payload.
- Steal every useful credential within reach.
- Use one victim’s credentials to reach the next victim.
TeamPCP did not need to break through the front door of every organization. In many cases, the victim’s own pipeline invited the attacker in, ran the attacker’s code, gave it access to secrets, and sometimes signed the result.
That is the uncomfortable lesson. Modern supply-chain attacks are not only dependency attacks. They are identity attacks delivered through automation.
Here is what defenders should take from them.
1. Treat CI as a production environment
A build runner is often described as temporary infrastructure. In practice, it may have access to package-publishing tokens, cloud credentials, GitHub tokens, SSH keys, container registries, Kubernetes clusters, and internal source code.
That makes it one of the most privileged machines in the company.
TeamPCP repeatedly targeted tools that already ran inside this environment. Trivy and Checkmarx KICS are security products, but that did not make them safer to execute. It made them more valuable targets. A compromised secret scanner is still a secret scanner; it is simply scanning for someone else.
The practical response is to reduce what any single job can reach:
- Separate build, test, and release jobs. Untrusted code should never run in the same trust boundary as publishing credentials.
- Prefer short-lived, job-specific identities over long-lived personal access tokens.
- Give the default
GITHUB_TOKENread-only permissions and add write permissions only where a job genuinely needs them. - Use ephemeral runners and destroy them after each job.
- Restrict outbound network access during dependency installation and builds.
That last control deserves more attention. Most of the payloads in this campaign needed to call home. If a package-install job can reach only the package registry and a small set of approved services, a successful upstream compromise has far less room to become a breach.
2. A mutable tag is remote code execution with better branding
uses: vendor/action@v1 looks reassuring. So does FROM node:20, tool:latest, or an automatically updated IDE extension from a familiar publisher.
None of those references identify immutable code. They identify names that can later point to different code.
That distinction was central to the TeamPCP campaign. Once the attackers controlled a trusted publishing path, downstream systems continued pulling from the expected project and publisher. The label stayed the same. The content changed underneath it.
Pin the things that execute inside privileged environments:
- Pin GitHub Actions to a full commit SHA. Tools such as ratchet can automate the rewrite and subsequent updates.
- Pin container images to a digest, not only a tag.
- Use
npm ciin CI and container builds so the lockfile is enforced rather than quietly rewritten. - Use hash-checking mode for Python dependencies with
pip --require-hashes. - Disable automatic IDE-extension updates where the extension runs in a sensitive development environment.
Pinning takes a little more upkeep, so automate the update PRs with Renovate or Dependabot, review the change, and keep the reference immutable between updates. The answer to update fatigue is controlled automation, not invisible code replacement.
3. A valid signature can still describe a malicious build
Some malicious releases connected to this campaign carried valid provenance. That sounds contradictory until we separate two questions:
- Where was this artifact built?
- Was that build authorized and safe?
Provenance can answer the first question honestly while the answer to the second is still no. If an attacker steals a CI identity or runs a trusted publishing workflow from an untrusted branch, the resulting attestation may be cryptographically valid. It proves that the compromised system built the package.
This does not mean Sigstore, SLSA, or trusted publishing failed. It means verification cannot stop at “an attestation exists.”
For release workflows:
- Scope OIDC trust to the specific repository and workflow that publishes.
- Require the workflow to run from a protected release branch or tag.
- Verify the source ref recorded in the provenance, not just the repository name.
- Put publishing behind a protected GitHub Environment with required reviewers.
- Keep the workflow’s token below admin scope. An admin token can often weaken or remove the controls intended to constrain it.
- Alert when a new branch, workflow file, or environment configuration participates in a release.
Provenance is evidence. Defenders still need to decide whether the story told by that evidence is acceptable.
4. Your IDE configuration is executable code
The later waves made a quiet but important jump from package-install hooks to developer-tool configuration.
Files such as .vscode/tasks.json, .claude/settings.json, .gemini/settings.json, and .cursor/rules/ can cause tools to execute commands when a folder opens or an agent session begins. A developer may never run the project. Opening it in the wrong tool can be enough.
This is particularly dangerous with AI coding agents. These tools are designed to read repository instructions and take actions on the developer’s behalf. A malicious instruction file can therefore become both prompt injection and code execution.
Treat these paths the same way you treat shell startup files or CI workflows:
- Require CODEOWNERS review for IDE, agent, MCP, and workflow configuration.
- Flag newly added or changed auto-run hooks in pull requests.
- Do not open untrusted repositories with workspace automation enabled.
- Review a repository in a non-executing environment before attaching an IDE or coding agent.
- Monitor coding-agent and IDE processes that spawn shells, package managers, or unexpected network connections.
Most organizations would investigate a pull request that modifies .github/workflows/release.yml. Far fewer apply the same scrutiny to .claude/settings.json. Attackers have already noticed the gap.
5. Give new packages time to become someone else’s incident
Several malicious TeamPCP releases were available for minutes, not days. That was enough to reach users running fresh installs or automated dependency updates. It was not enough to survive a modest waiting period.
A minimum release age is one of the simplest controls available. Configure your package manager and update bots to reject or delay versions published within the last 24 to 72 hours. The precise window depends on how quickly you need updates, but even one day would have outlasted several major exposure windows in this campaign. Both pnpm and newer npm releases support this directly.
Pair the delay with deterministic installation:
- Use
npm ci, committed lockfiles, and lockfile-integrity checks. - Default npm installs to
--ignore-scripts, with a small allowlist for dependencies that genuinely require lifecycle scripts. - Restrict package installation to an approved registry or internal mirror.
- Prevent install-time processes from making arbitrary outbound connections.
There is an important ecosystem caveat: --ignore-scripts is not a universal supply-chain control. Python .pth files can execute code when the interpreter starts, and native wheels can run compiled initializers when imported. For Python, enforce hashes, prefer curated binary sources, inspect unexpected .pth and native-extension files, and keep the same egress restrictions in place.
The point is not to make updates impossible. It is to stop treating “published five minutes ago” as a reason to run code immediately inside a privileged environment.
6. Credential rotation is an incident-response procedure, not a password reset
One of the most consequential moments in the campaign came after the first compromise had already been detected. A credential was rotated, but attacker access survived. That surviving access helped enable the next major strike.
Rotating a token is not complete when a new token has been created. It is complete when the old credential has been revoked, the revocation has been verified, every copy of the credential has been replaced, and the system that exposed it is clean.
After a CI or developer-tool compromise:
- Isolate affected hosts and runners.
- Perform rotation from a known-clean system.
- Revoke old credentials before issuing replacements where the platform allows it.
- Query the provider’s API to verify which tokens and sessions remain active.
- Rotate every credential the compromised process could read, not only the credential known to have been used.
- Review downstream systems for use of the stolen identity before declaring containment.
The later TeamPCP-derived Miasma and Hades tooling added another complication: invalidating a token could trigger destructive behavior on an infected host. In that scenario, revoking first may cause damage. The safe sequence is to isolate the host from the network, preserve evidence, contain the destructive mechanism, and then rotate from clean infrastructure.
Your response plan should account for both cases. “Rotate credentials” is not a runbook.
7. Detect the behavior, not the campaign name
Domains, hashes, package names, and operator branding are useful during an active investigation. They age quickly. The behavior lasts longer.
Across this campaign, useful detection opportunities included:
- A package manager or build tool spawning a shell,
curl,wget, or an unexpected runtime. - Bun appearing on a CI runner that has never used Bun.
- Python making outbound connections at interpreter startup.
- A build process reading shell history, SSH keys, cloud configuration, package-manager credentials, or
/proc/<pid>/mem. - New workflow, IDE, MCP, or AI-agent configuration files appearing in a repository.
- A maintainer identity creating public repositories or publishing packages outside the normal release process.
- A release workflow running from a new branch or unexpected commit.
- Security tooling losing telemetry while the process still appears healthy.
These are technique-level signals. They remain useful when the attacker changes a domain, recompiles a payload, or removes the TeamPCP branding.
What I would do this week
Trying to deploy every possible supply-chain control at once is a reliable way to deploy none of them. Start with the changes that reduce the largest amount of risk:
- Inventory every human and machine identity that can publish packages, modify workflows, or write to release branches. Move the human accounts to passkeys.
- Pin GitHub Actions and container images to immutable SHAs or digests.
- Add at least a 24-hour minimum release age to package managers and dependency-update bots.
- Require review for changes to workflows, dependency manifests, lockfiles, IDE configuration, and AI-agent instructions.
- Remove secrets from untrusted build jobs and restrict network egress from runners.
- Test a credential-rotation exercise that begins with host isolation and ends with API-verified revocation.
None of these controls is glamorous. That is part of their value. They do not depend on recognizing TeamPCP, predicting the next package, or buying a perfect detection product. They reduce the amount of trust automatically granted to code you did not write.
The campaign may end. The access does not.
The arrests may be the end of TeamPCP as a name or as an organized group. They do not revoke the credentials already stolen, remove persistence from infected developer machines, or prevent other actors from reusing the same techniques and leaked tooling.
The lasting lesson is simple: every trusted component should have a limit.
A package should not be able to reach every secret on a runner. A build job should not also be a release administrator. A valid signature should not bypass policy. An IDE should not silently execute repository instructions. A newly published version should not flow directly into production.
Supply-chain security improves when trust becomes specific, short-lived, and observable. Fewer credentials. Smaller permissions. Immutable references. Slower adoption. Less automatic execution.
That is not as dramatic as tracing an attacker across 37 incidents. It is how we stop the next chain from getting that long.
Explore the full sourced timeline, technical details, indicators, and free defender toolkit at teampcp.pluto.security.

