In March 2025, the tj-actions/changed-files attack hit a popular action used by more than 23,000 repositories and, within 24 hours, turned affected builds into a dump of their own secrets, printed straight into the workflow logs. The technique was not sophisticated. A stolen access token let the attacker rewrite the action to read the runner’s memory and write whatever secrets it found into those logs. No zero-day required.
That attack follows a pattern that has recurred in every major GitHub Actions supply chain incident since CodeCov in 2021. Six attacks documented in Cimon’s threat log, spanning March 2024 through March 2026, all used the same four-phase playbook. The entry points differ. The behavior inside the runner does not.
Understanding that pattern, and where existing controls fall short, is the foundation of an effective defense.
Key takeaways
- GitHub Actions supply chain attacks follow a four-phase playbook: initial access, execution, exfiltration or tampering, and persistence. Every documented incident maps to this pattern regardless of entry point.
- The build runner is the unmonitored gap. Most teams apply strong controls to source code and production environments. The build environment itself, where attacks actually execute, gets little to no visibility.
- SHA pinning, token scoping, and pre-build scanning are necessary but insufficient. None of these controls observes what executes inside a runner during a build. The attack happens there.
- Runtime monitoring with eBPF is the only mechanism that detects and blocks these attacks at the execution layer, regardless of how the attacker entered. Cimon applies kernel-level visibility to CI/CD runners, catching the specific behaviors each attack in this piece relied on: unauthorized network connections, credential file reads, and environment variable injection.
Why CI/CD Pipelines Are the Most Valuable Target in the Software Supply Chain
Build pipelines hold something production servers rarely do: simultaneous access to source code signing keys, cloud provider credentials, package registry tokens, and deployment secrets. Compromise a pipeline and you have not breached one system; you have breached the trusted intermediary between source code and every piece of software it ships.
GitHub Actions amplifies this exposure. The platform runs over 5 million workflows daily. Every third-party action a workflow references is a potential supply chain entry point. Every pull_request_target trigger is a potential trust boundary violation. Attackers do not need to compromise your code. They need to compromise something your pipeline trusts, a substantially easier problem.
The tj-actions attack is instructive. It began four months before discovery when an attacker stole a SpotBugs maintainer’s personal access token via a pull_request_target exploit and waited. When the modified action finally ran, every build that used it during the window dumped its secrets into the logs. More than 23,000 repositories referenced the action, and the ones with public logs exposed those secrets to anyone watching. We documented the full attack chain in our tj-actions/changed-files supply chain attack: the complete guide.
The Four-Phase Attack Pattern
Every major GitHub Actions supply chain attack follows the same four phases. Strip away the specific tooling and entry points and the execution is identical each time.
| Phase | What happens | Real example | What stops it |
|---|---|---|---|
| 1. Initial access | Attacker gains code execution inside a privileged runner | Compromised marketplace action, pull_request_target trigger abuse, typosquatted dependency | SHA pinning, action vetting, least-privilege tokens |
| 2. Execution | Attacker code runs with full access to GITHUB_TOKEN and all injected secrets | Malicious script spawns subprocesses, reads env vars, calls external endpoints | eBPF process monitoring, hardening rules (Cimon) |
| 3. Exfiltration or tampering | Attacker steals credentials or modifies build artifacts | Base64 HTTP exfil, /proc/PID/mem read, GITHUB_ENV injection, artifact replacement | eBPF network egress blocking, file access controls (Cimon) |
| 4. Persistence | Attacker installs backdoors or poisons downstream packages | Post-install scripts publishing malicious packages under compromised registry identity | Build-time SBOM observation, process anomaly detection (Cimon) |
This pattern exposes the precise limitation of perimeter-focused controls. SHA pinning addresses initial access. Token least privilege constrains the blast radius during the exfiltration phase. Neither control has any visibility into phase two, execution, which is where the attack does its actual work inside the runner.
Six Documented Attacks, One Pattern
Cimon monitors CI/CD build environments at the kernel level via eBPF, observing process execution, network connections, and file access in real time. The following six incidents from Cimon’s threat log illustrate the attack pattern in practice. In each case, the attack behavior that caused the breach was visible and blockable at the runtime layer, regardless of how the attacker entered.
| Incident | Date | Attack method | What it exfiltrated or tampered | Cimon detection layer |
|---|---|---|---|---|
| trivy-action / TeamPCP | Mar 2026 | Compromised GitHub Action | CI secrets dumped; /proc/PID/mem read to extract in-memory secrets from runner processes | File access to /proc/PID/mem blocked in prevent mode; hardening rule fired immediately |
| LiteLLM (PyPI backdoor) | Mar 2026 | Malicious PyPI package | Credential stealer ran at import time, exfiltrating cloud tokens and wallet keys to attacker domains; systemd backdoor installed for persistence | Outbound connection to the attacker domain blocked; process launched on Python import flagged as anomalous |
| Telnyx / TeamPCP | Mar 2026 | Malicious PyPI package (same TeamPCP payload as LiteLLM) | Backdoored release ran the LiteLLM-style stealer at import time and called out to an attacker endpoint connected to raw C2 IP addresses | Outbound connection to the attacker endpoint blocked; process spawned on import flagged as anomalous |
| tj-actions/changed-files | Mar 2025 | Compromised Action tag (23,000+ repos) | Runner.Worker memory dumped, double-base64-encoded, and printed into the build logs (public for public repos) | Outbound fetch of the memdump script from a non-allowlisted gist blocked; process reading Runner.Worker memory and the base64 step both flagged |
| reviewdog/action-setup | Mar 2025 | Compromised Action | /proc/PID/mem read to extract runner process memory | Hardening rule blocked /proc/PID/mem access without requiring learned baseline |
| Ultralytics | Dec 2024 | Script injection via pull_request_target (crafted branch name) | Branch-name payload ran curl-to-bash in the runner, stole CI/CD secrets, and poisoned the build to ship an XMRig cryptominer to PyPI | Injected curl-to-bash subprocess flagged; outbound fetch of the payload script blocked |
trivy-action and TeamPCP (March 2026)
Two separate actions, both compromised by the threat actor group TeamPCP, were used to dump CI secrets and read process memory from GitHub Actions runners. The key technique: reading /proc/PID/mem, a Linux kernel interface that exposes raw process memory. Secrets already loaded into runner memory and masked in log output remain in memory. That masking provides no protection against a direct memory read.
Cimon’s eBPF-based file access monitoring observes reads of /proc/PID/mem at the kernel level and blocks them in prevent mode before any data leaves the system. This is a hardening rule; it fires immediately on installation without requiring a learned behavioral baseline.
LiteLLM PyPI backdoor (March 2026)
A malicious release of LiteLLM, a widely used AI gateway package, carried a payload that ran the moment Python imported the module. One version went further and dropped a .pth file, so the code executed on any Python invocation in the environment, whether or not anything imported LiteLLM.
Once running, it harvested credentials, including cloud tokens and crypto wallet keys, and sent them to attacker domains such as models.litellm.cloud and checkmarx.zone. It also installed a systemd service for persistence. Static analysis passed; the behavior only showed up at runtime, inside the runner.
Cimon’s network egress monitoring blocked the outbound connection to a domain no build step had ever contacted. The process that came to life on import, well outside the pattern of a normal dependency install, was flagged as anomalous on its own, with or without the network signal.
Telnyx and TeamPCP (March 2026)
Days after LiteLLM, the same actor backdoored the telnyx package on PyPI with the same kind of payload. The malicious release ran its stealer when Python loaded the module and reached out to an attacker-controlled endpoint to hand over whatever credentials it found in the runner. Same playbook, different package name.
Cimon’s process execution monitoring works at the syscall level, so it sees the payload start regardless of how it was launched. The outbound connection to an endpoint outside the pipeline’s established baseline was flagged and blocked by network egress enforcement.
tj-actions/changed-files (March 2025)
The attacker modified the action to pull a script from a public gist and run it, dumping the memory of the Runner.Worker process, extracting the secrets loaded there, encoding them in double base64, and printing them straight into the build logs. More than 23,000 repositories referenced the action, and any whose logs were public exposed those secrets to anyone reading. There was no callback to an attacker server in the main variant; the logs themselves were the exfiltration channel.
Cimon blocks this in more than one place. The outbound fetch of the script from a gist that no build step had ever contacted is stopped by network egress enforcement. The process reaching into Runner.Worker memory is flagged as a memory read no build tool should perform, and the base64 encoding step is flagged as anomalous process behavior. Any one of these fires before the secrets reach the logs. For the full attack chain and remediation steps, see our complete guide to the tj-actions/changed-files incident.
reviewdog/action-setup (March 2025)
A compromised action read /proc/PID/mem to extract secrets from runner process memory, the same technique as the trivy-action attack one year later. This incident confirmed the vector was being actively developed and reused by attackers as a reliable method for extracting secrets that mask-based controls cannot protect.
Cimon’s hardening rule for this technique fires without requiring a learned behavioral baseline, providing immediate protection from the first pipeline run.
Ultralytics (December 2024)
The entry point was a GitHub Actions script injection. A workflow that ran on pull_request_target pulled the pull request’s branch name straight into a shell command without sanitizing it, so a branch named with a crafted curl-to-bash payload executed in the privileged runner. That code stole the project’s CI/CD secrets and poisoned the build, and the result was an XMRig cryptominer shipped inside Ultralytics releases 8.3.41 and 8.3.42 on PyPI.
This is the opposite of a quiet attack. It spawns a subprocess and makes an outbound call to fetch its payload, both of which Cimon sees at the kernel level. Egress enforcement blocks the fetch to an endpoint the pipeline never normally contacts, and the injected curl-to-bash process is flagged the moment it runs.
Why Static Controls Are Insufficient
Every standard response to GitHub Actions supply chain attacks reaches for the same mitigations: pin actions to commit SHAs, restrict GITHUB_TOKEN permissions, audit pull_request_target usage. These controls are correct. They are also insufficient for the following reasons.
SHA pinning stops moved tags, not poisoned releases
Pinning an action to a commit SHA means a moved or force-pushed tag cannot quietly swap the code under you. That is exactly the failure mode in tj-actions and trivy-action, where the attacker repointed existing tags at malicious commits, so pinning would have blunted both of them.
Where pinning runs out of the road is everything after that. It says nothing about the PyPI side of the supply chain, where LiteLLM, telnyx, and the Ultralytics releases were backdoored at publish time, and it says nothing about what an action does once it executes. Pinning narrows one entry point, it does not watch the build.
Token least-privilege limits blast radius, not exfiltration
Restricting GITHUB_TOKEN permissions reduces what an attacker can do with a stolen token. It does not prevent that token from being stolen. It does not prevent other secrets injected into the workflow environment from being exfiltrated. The tj-actions attack targeted all runner environment secrets, not the GITHUB_TOKEN specifically.
Pre-build scanning does not see the build
SAST, SCA, and dependency scanning operate on code and manifests before the build runs. None of these tools observe what executes inside the runner. A malicious post-install script, a subprocess spawned by a compromised action, an unauthorized outbound network call, a read of /proc/PID/mem, none of these are visible to any tool running before the build. The attack executes inside the build. The control has to be there too.
How eBPF Runtime Monitoring Closes the Gap
eBPF, or extended Berkeley Packet Filter, is a Linux kernel technology that lets programs run safely inside the kernel itself, with direct visibility into system calls, network events, file operations, and process lifecycle events as they happen. Unlike logging tools that read output after the fact, eBPF hooks into kernel operations at the moment they occur, which means it can observe and block behavior before it completes. For CI/CD security, that distinction matters: the attacks documented in this piece all executed and exfiltrated data inside a running build, in a window that no pre-build or post-build tool ever sees. eBPF is the only mechanism that puts a security control inside that window.
Cimon uses eBPF to monitor three categories of runner behavior:
- Process execution: every process spawned during the build, including subprocesses launched by third-party actions, package managers, and build tools. Cimon builds a process tree per job and flags any execution that deviates from the established baseline or matches a known attack pattern.
- Network connections: every outbound connection from the runner, tied to the specific process that initiated it. Cimon establishes a network baseline per pipeline and blocks connections to endpoints outside the allowlist. This stops the gist fetch that pulled the memory-dump script in tj-actions and the callbacks to attacker domains in LiteLLM and Telnyx.
- File access: every file read and write inside the runner, including reads of /proc/PID/mem and writes to GITHUB_ENV. The /proc/PID/mem reads are the file operations that trivy-action, reviewdog, and tj-actions all relied on. eBPF observes them at the kernel level before the operation completes.
Cimon’s Two-Phase Protection Model
Detect mode
Cimon observes pipeline behavior without blocking anything. Every process, network connection, and file access is logged and used to build a security profile for that pipeline. Security reports show exactly what the pipeline does: which processes it spawns, which endpoints it calls, which files it reads and writes.
Detect mode is the right starting point for teams new to runtime pipeline monitoring. It surfaces unexpected behavior without disrupting CI/CD workflows and generates the data needed to configure an accurate prevent mode policy.
Prevent mode
Once a pipeline’s normal behavior is established, Cimon enforces it. Any deviation from the learned profile triggers a block: unauthorized network call dropped, unexpected process killed, prohibited file access denied. The pipeline fails with a security violation rather than completing silently with attacker-controlled behavior.
Cimon’s hardening rules run independently of the detect-then-prevent cycle. These are pre-built detections for known attack patterns: /proc/PID/mem reads, GITHUB_ENV injection, reverse-shell subprocess patterns, and base64 encoding followed by outbound HTTP. Hardening rules fire immediately on installation, before the learning phase completes.
Hardening GitHub Actions Pipelines in Practice
Runtime monitoring with eBPF is the most effective control for the attack behaviors documented here. It complements conventional hardening, not replaces it.
Layer 1: Structural controls
- Pin all third-party actions to commit SHAs, not version tags. Use Dependabot to keep pins current automatically.
- Restrict GITHUB_TOKEN permissions to the minimum required per workflow. Default to read-only and elevate only where explicitly needed.
- Audit pull_request_target and workflow_run triggers across all repositories. These run fork code in a privileged context and have been the entry point in multiple documented incidents.
- Score and vet third-party actions before adoption. Prefer actions with recent maintenance, high adoption, and signed commits.
Layer 2: Runtime monitoring with Cimon
- Add Cimon as the first step in every workflow with access to production secrets or a trusted execution context.
- Start in detect mode to establish behavioral baselines without disrupting pipelines. Review security reports to surface unexpected network calls or process behavior.
- Enable prevent mode once baselines are validated. Hardening rules are active immediately regardless of mode.
- Connect Cimon to the Cycode Agentic Development Security Platform for organization-wide pipeline visibility, centralized policy management, and incident response workflows when violations occur.
Adding Cimon to a workflow is a single step:
- name: Run Cimon
uses: cycodelabs/cimon-action@v1
with:
prevent: false # set to true for prevent mode Full configuration options and prevent mode setup are in the Cimon docs.
Beyond Runtime Protection: SLSA Provenance and Runtime-Observed SBOMs
Because Cimon observes everything that executes during a build at the kernel level, it generates two compliance artifacts that static tools cannot: a runtime-observed software bill of materials capturing what actually executed during the build (not what the manifest declared), and signed SLSA provenance attestations binding the build artifact to its source, environment, and process.
Both are generated in the same pipeline step that provides runtime protection. No additional toolchain. No separate signing workflow. For teams working toward EO 14028 compliance or NIST SSDF requirements, Cimon covers both in a single installation.
The Build Environment Is the Security Perimeter Most Teams Overlooked
Every attack in this piece hit teams with mature security practices. They scanned their code. They managed their dependencies. They used secrets management. What they lacked was visibility into what executed inside the build runner, the one environment where all of those controls are bypassed by design.
That gap now has a solution. eBPF brings kernel-level visibility to CI/CD pipelines with a single workflow step, no pre-installed agent, no persistent infrastructure, negligible performance overhead.
Cimon is free, open, and operational in under five minutes. The Cimon docs cover everything from getting started in detect mode through enabling prevent mode and connecting to the Cycode platform for org-wide policy management. The next GitHub Actions supply chain attack will follow the same four-phase pattern as every one documented here. The question is whether your build environment will be watching when it does.
Frequently Asked Questions
Does Cimon work on self-hosted GitHub Actions runners?
Yes. Cimon's eBPF-based monitoring works on any Linux runner: GitHub-hosted runners, self-hosted runners, and Azure Pipelines agents. The kernel-level instrumentation does not depend on the runner infrastructure provider.
Self-hosted runners raise the stakes. They often persist between jobs, so a compromise can linger and reach into later builds. Watching process, network, and file activity at the kernel level catches that movement on hardware you own and reuse.
What is the difference between SHA pinning and runtime monitoring for GitHub Actions security?
SHA pinning and runtime monitoring operate at different phases of the attack pattern and protect against different things. SHA pinning is a structural control that prevents a moved or force-pushed tag from silently swapping the code an action executes. It addresses the initial access phase: if an attacker repositions a tag at a malicious commit, pinning means your workflow still runs the version you approved. The tj-actions and trivy-action attacks would both have been blunted at the entry point by consistent SHA pinning.
Runtime monitoring addresses everything that happens after initial access. Once attacker code is executing inside a runner, whether through a compromised action, a backdoored PyPI package, or a script injection, SHA pinning has no visibility into what that code does. It cannot see a process reading /proc/PID/mem, an unauthorized outbound connection to an attacker domain, or an environment variable being injected via GITHUB_ENV. eBPF-based runtime monitoring like Cimon watches those behaviors at the kernel level as they occur and blocks them before they complete. The short answer: SHA pinning tries to keep the attacker out of the runner. Runtime monitoring assumes one gets in and stops the next move. Both layers are necessary because they fail in different ways.
How did the tj-actions/changed-files supply chain attack work?
The tj-actions/changed-files attack followed a four-phase chain that began months before anyone noticed it. An attacker first stole a SpotBugs maintainer's personal access token by exploiting a pull_request_target trigger, which runs fork code in a privileged context. The attacker used that token to modify the tj-actions/changed-files action, repositioning existing version tags to point at a malicious commit instead of the legitimate one.
When the modified action ran in a victim's workflow, it fetched a script from a public GitHub Gist and executed it inside the runner. That script reads the memory of the Runner.Worker process directly via /proc/PID/mem, a Linux kernel interface that exposes raw process memory including secrets that had already been masked in the workflow logs. The script then double base64-encoded whatever it found and printed it straight into the build logs. For repositories with public logs, those secrets were visible to anyone watching. More than 23,000 repositories referenced the action during the window, and no zero-day was involved at any point: a stolen token, a repositioned tag, a memory read, and a log write were all it took. The full attack chain with remediation steps is documented in the complete tj-actions/changed-files incident guide.
What is the performance impact of running Cimon in a pipeline?
eBPF programs are verified by the Linux kernel before execution and run in a sandboxed context with strict resource limits. The overhead for most pipelines is negligible. Cimon is built for production-grade pipeline use.
Most of that efficiency comes from filtering events inside the kernel before they reach user space. A busy build can fire thousands of syscalls a second. Cimon drops the irrelevant ones early, so the agent stays light even under heavy load.
Does Cimon require access to source code or build definitions?
No. Cimon operates as a GitHub Action or Azure Pipelines task that observes build runner behavior at the kernel level. It does not require source code access, build definition access, or credentials beyond what the runner already holds.
Because it watches behavior instead of reading code, your language and build tooling do not matter. A Go build, a Node install, and a Python packaging step all show up as the same process, network, and file events. There is nothing per-project to configure before it starts working.
How does Cimon handle pipelines that legitimately make external network calls?
In detect mode, Cimon learns which external endpoints are part of normal pipeline behavior. In prevent mode, it enforces that baseline. Legitimate external calls observed in detect mode are allowlisted automatically. Teams can also manually configure allowlists before enabling prevent mode.
When a build needs a new endpoint later, it shows up in the security report tied to the process that called it. You can see what is asked for before you decide anything. Adding it to the allowlist is a one-line change, and the rest of the baseline stays intact.
Is Cimon a replacement for SHA pinning and least-privilege token configuration?
No. These operate at different layers. SHA pinning and token scoping are structural controls that reduce the attack surface before the build runs. Cimon is a runtime control that operates inside the build environment. Both layers are necessary. They protect against different phases of the attack pattern.
Think of it as defense in depth. Pinning and least privilege try to keep an attacker out of the runner, while Cimon assumes one gets in and blocks the next move, the memory read, or the unlisted callout. Keep both, because they fail in different ways.
