Actions-Cool/Issues-Helper is Compromised
)
When Every GitHub Tag Lies
May 19, 2026
Another GitHub Action has been hijacked, and it's a textbook example of why "pin to a version" is not the same as "pin to known-good code."
On the StepSecurity blog, researchers disclosed that
actions-cool/issues-helper — a popular issues-management Action — was compromised by an attacker who gained write access to the repository and rewrote every existing tag to point to a single imposter commit that is not part of the action's normal commit history. A sibling project from the same maintainer, maintain-one-comment, was hit the same way by the same actor.If you reference either of these by version
@v3, @v3.5.0, @main), your next CI run pulls the attacker's code.The attack vector
The mechanics are simple and that's what makes them dangerous:
1. The attacker gained repository write access. How is not yet public, but the outcome is: they could move tags.
2. They created an imposter commit off the normal history — a commit that no human reviewed, no PR introduced, and that doesn't appear in the linear history of the default branch.
3. They moved every tag
v1, v2, v3, all minor and patch tags) to point at that imposter commit. Any workflow resolving a version reference now resolves to the malicious code.4. The payload executes inside your runner. When the compromised action executes, it:
- Downloads the bun JavaScript runtime to the runner.
- Reads the memory of the
Runner.Workerprocess — the GitHub Actions component that holds decrypted secrets while a job is in flight. - Exfiltrates the harvested credentials over HTTPS to
t.m-kosche.com.
The
read Runner.Worker memory step is the part that should make every security team uncomfortable. GitHub's masking only redacts secrets from logs. Once a secret is decrypted into the worker process, any code running on the runner with sufficient privilege can read it directly out of memory. Branch protection, required reviews, and code scanning all happen before this point — they don't help once untrusted code is executing on your runner.This is the same pattern we saw in
tj-actions/changed-files earlier in the year, and the same pattern we'll see again. CI/CD runners are a high-value target because they hold the keys to every downstream system — cloud accounts, registries, deploy targets, signing infrastructure — and they run untrusted code by design.If you're an end user: remediation
Treat this as an incident, not a notification.
1. Audit usage across every repo and every workflow.
# In each repo:git grep -nE 'actions-cool/issues-helper|actions-cool/maintain-one-comment'Don't forget reusable workflows, composite actions, and forks.
2. Rotate every secret that any affected workflow had access to.
Not just the ones you "think" were used — every secret available to the job at the time it ran. That includes:
GITHUB_TOKENis short-lived, but anything it could write (releases, packages, deploy keys) should be re-checked for tampering.- Cloud credentials (AWS, GCP, Azure), including OIDC-issued short-lived tokens — pivot through CloudTrail / audit logs.
- Package registry tokens (npm, PyPI, crates.io, GHCR).
- Signing keys, deploy keys, third-party API tokens, anything in
secrets.*.
3. Pin by commit SHA, not by tag, going forward.
# Vulnerable — tag can be moved- uses: actions-cool/issues-helper@v3
# Better — SHA cannot be moved- uses: actions-cool/issues-helper@a3b8...verify-this-shaSHA pinning would have completely prevented this attack. It's the single highest-leverage change you can make to your workflows today.
4. Investigate before you assume you're safe.
Pull workflow run logs for any job that used the action between the tag-move and the disclosure. Look for outbound connections to
t.m-kosche.com, unexpected bun downloads, or any process accessing /proc/<pid>/mem of the worker.5. Look at the second-order blast radius.
If a compromised job had permission to push to a registry, modify infrastructure, or sign releases, the attacker's reach extends beyond the secrets themselves. Diff recent releases and artifacts against expected outputs.
Why this keeps happening
The structural problem is that CI runners execute untrusted code with privileged credentials and unrestricted network egress.
- Static scanning doesn't help — the malicious code only exists at runtime, after a tag was moved.
- Branch protection doesn't help — the attacker had write access to tags, which is a separate (and often overlooked) permission boundary.
- Secret scanning doesn't help — by the time secrets are in
Runner.Workermemory, they're already exposed. - Required reviews don't help — no review happened on the imposter commit because it wasn't introduced through a PR.
The exfiltration step is the one consistent chokepoint. The attacker has to get the stolen credentials off the runner. That means an outbound network connection to a domain you do not legitimately need.
How CargoWall prevents this class of attack
CargoWall is an open-source runtime egress firewall for GitHub Actions runners, enforced in the kernel with eBPF. It assumes the code running on the runner may be malicious, and constrains what that code can do on the network.
For the
actions-cool/issues-helper attack specifically, three CargoWall behaviors break the kill chain:1. Deny-by-default egress blocks the exfiltration.
The whole attack relies on shipping decrypted secrets to
t.m-kosche.com. With a CargoWall policy of default-action: deny and only your legitimate destinations allowed github.com, your package registries, your cloud APIs), the HTTPS POST to the attacker's domain is dropped at the kernel level before it reaches the network. The secrets stay on the runner.- uses: code-cargo/cargowall-action@v1 with:default-action: deny allowed-hosts: | github.com, api.github.com, registry.npmjs.org2. The
bun runtime download is blocked at the source.The payload's first step is to pull down
bun from its CDN. If bun.sh and its download mirrors aren't in your allowlist, that download fails — the loader never even gets to the credential-harvesting stage. Egress firewalling collapses the attack early, before the dangerous code runs.3. Audit logs give you forensic ground truth.
Even if you start in audit mode (no blocking, log only), CargoWall writes an NDJSON record of every outbound connection with full process attribution — which PID initiated it, which binary, what hostname, what destination. If a compromised action ever ran in your environment, the audit log tells you definitively whether it phoned home, and which job it ran in. No more guessing from incomplete workflow logs.
Crucially, none of this depends on knowing the attacker's domain in advance. CargoWall is an allowlist, not a blocklist. Whether the C2 destination is
t.m-kosche.com today or something else next month, if it isn't on your allowlist, it doesn't get a packet.Scaling protection across an org: CodeCargo enterprise
Adding
code-cargo/cargowall-action to one workflow is easy. Getting it onto every workflow in every repo across an organization — and keeping the policies coherent as teams add new dependencies — is the harder problem. That's where the CodeCargo platform comes in.With a CodeCargo enterprise plan, CargoWall becomes a centrally managed control plane, not a per-workflow opt-in:
- Provision policies from a single dashboard. Define your allowlist once and apply it to every repo and workflow in your org. No PR to every repository, no copy-paste drift between teams, no chance that the one workflow nobody remembered is the one that pulls a compromised action.
- Hierarchical overrides. Set a strict org-wide baseline
github.com, your registries, your cloud), then let individual repos or jobs request narrowly-scoped additions. Reviews and approvals stay in the platform, not buried in workflow YAML. - Inheritance and exceptions are visible. You can see, at a glance, which workflows are running which policy, which are in audit vs. enforce mode, and which have requested exceptions — instead of grepping across hundreds of YAML files.
- Coverage as a compliance artifact. SOC2 and FedRAMP auditors want evidence that pipeline controls exist and are uniformly applied. Central provisioning turns "we have a firewall on most of our workflows" into "here is the policy, here are the workflows it's bound to, here are the audit logs."
Attribution: not just "what was blocked," but "who tried"
Blocking the exfiltration is half the value. The other half is knowing what happened.
When the compromised
actions-cool/issues-helper payload tries to phone home from inside one of your runners, CargoWall doesn't just drop the packet. The CodeCargo platform shows you, for that exact event:- Which workflow run the attempt came from (org, repo, branch, run ID, commit SHA).
- Which job within that run, and which step was executing at the moment the connection was attempted.
- Which action (and pinned version or SHA) introduced the step that initiated the connection.
- Which process on the runner — PID, binary path, command line — actually called
connect(). The eBPF cgroup hooks attribute every connection to its originating process, so the spawnedbunruntime doesn't get to hide behind the parent shell. - Where it tried to go — hostname (as resolved by the runner) and destination IP, with the DNS resolution path that produced it.
This turns "your CI got attacked" from a frantic forensics exercise into a one-screen incident: job 4812, step
actions-cool/issues-helper@v3, spawned /tmp/bun, attempted HTTPS to t.m-kosche.com — denied at 14:02:11Z. You know which action to remove, which repos to scrub, and which credentials were in the runner's environment at the moment of the attempt — without piecing it together from masked workflow logs after the fact.That signal is also how you catch the next compromised action before disclosure. A spike of denied connections from a previously well-behaved action is a leading indicator, not a lagging one.
The takeaway
Tag-moving attacks are not an exotic edge case. They've happened repeatedly, and they will keep happening as long as:
- Action repositories can have their tags rewritten,
- Workflows pin by version instead of SHA, and
- Runners have unrestricted network egress with decrypted secrets in memory.
You can — and should — fix the first two on your end (pin by SHA, audit your dependencies). But neither of those helps when a transitive action, a freshly added integration, or a teammate's PR slips in a versioned reference. Defense in depth says you also need to assume the code on your runner might be hostile and stop it from talking to the outside world.
That's what CargoWall is for.
Get started: add the CargoWall GitHub Action your GitHub Actions workflows.
Read the original disclosure: StepSecurity — actions-cool/issues-helper GitHub Action compromised
C
CodeCargo Team
The CodeCargo team writes about GitHub workflow automation, developer productivity, and DevOps best practices.
)