top of page

Hunt Mini Shai-Hulud Actions: mutable tags that woke back up

Writer: Inception Security
Inception Security
4 hours ago
8 min read
Neon illustration of a GitHub Actions version tag quietly pointing at an attacker's commit inside a CI pipeline

In May, GitHub disabled two popular issue-housekeeping Actions after the Mini Shai-Hulud crew moved their release tags onto imposter commits. Disabling the repositories stopped the downloads. It did not clean the tags, and four months later that distinction turned out to be the entire story. On 16 September both repositories became reachable again, with the same malicious 18 May commits still sitting behind the same version tags. Every workflow that still referenced actions-cool/issues-helper or actions-cool/maintain-one-comment by a mutable tag went straight back to pulling and running the payload on its next schedule or issue event. No attacker had to publish anything, or do anything at all. GitHub's dependency graph lists roughly 15,000 repositories depending on issues-helper alone.


Socket documented both the re-enable window and the second disable that followed, and GitHub has since taken both repositories down again, so new runs fail at job setup. That is worth something, but it is worth less than it sounds. Nothing about a disabled repository un-steals the secrets from jobs that ran successfully after 16 September.


For estates that run GitHub Actions, the practical questions are:


  • Do any owned workflows still reference actions-cool/issues-helper or actions-cool/maintain-one-comment by tag?

  • Did any of those jobs flip from seconds-long Set up job failures to multi-minute successes starting around 16 September?

  • Which secrets could those jobs read, and have they been rotated since?

  • Are third-party Actions pinned to full commit SHAs estate-wide, or is platform disable still treated as a clean restore?


How the attack works


Trusted housekeeping in CI


Both Actions do unglamorous work. One closes inactive issues, the other keeps a single bot comment current, and teams wire them into scheduled or issue-driven workflows and then never think about them again. Socket counts roughly 15,000 dependents on issues-helper alone in GitHub's dependency graph, and most of those references look like @v2.2.1 rather than a full commit SHA. That last detail is the actual vulnerability. Everything after it is consequence.


May 18 tag redirect


The attackers did not really compromise the code. They compromised the pointer to it, moving the release tags onto imposter commits. Researchers documented the payload as a wrapper that downloads Bun, runs an obfuscated index.js, reads whatever secrets the Actions runner can see, and ships them to t.m-kosche . com, a domain that overlaps with the wider Mini Shai-Hulud npm cluster. GitHub disabled both repositories on 19 May, and for the next four months a runner hitting either one would fail at Set up job with repository access blocked before a single step executed. Hold onto that failure, because its disappearance is the thing you can actually hunt for.


16 September re-enable, dirty tags intact


Socket's Karlo Zanki reports that both repositories became reachable again between roughly 09:09 and 16:16 UTC on 16 September, and the tags still resolved to the May content. actions-cool/issues-helper@v2.2.1 pointed at commit a0c53dd42fc842d2f9276c5a1d4f9a26abe8713d, with the obfuscated payload sitting in index.js, and runner logs from that window show a Bun install followed by bun run $GITHUB_ACTION_PATH/index.js. The clearest tell is what happened to the scheduled jobs. Workflows that had been failing in seconds on 14 and 15 September started succeeding in multi-minute runs on 16 September, with no change to the consumer workflow file at all. Socket could not confirm why the re-enable happened.


Second disable


Socket's 25 September update says both repositories are disabled on GitHub once more, so workflows that still reference them fail at job setup rather than running the payload. That stops new executions and nothing else. It does not repair a secret that left your runner during the reopen window.


Risk: CI secrets under mutable tags


Any organisation whose workflows still pin these Actions by tag, or that ran them on or after 16 September, is looking at CI secret exposure: GITHUB_TOKEN scopes, cloud keys, npm or PyPI tokens, and anything else the workflow could reach through its secrets context. The blast radius after 16 September includes unexpected commits, package publishes, and cloud activity under those identities. Scope it honestly, though. The population at risk is whoever ran tag-based references during the reopen window, not some implied Fortune 500 victim list, and clean commits that were SHA-pinned before 18 May were never touched by the tag move.


The real sting here is an assumption gap. Teams saw the May disable, filed it under handled, and stopped worrying, because a disabled repository feels like a closed incident. But the tags never healed, so when the repositories came back the same workflow YAML that looked completely unchanged started succeeding for the wrong reason. If your change management only diffs the workflow files you own, you will never see a tag that moved underneath you.


Mitigate and reduce risk


Search all owned .github/workflows/ for actions-cool/issues-helper@ and actions-cool/maintain-one-comment@. Treat every tag reference, including @v2.2.1, as affected.


Remove the Actions, or pin them to a verified full commit SHA that predates 18 May 2026. For these two there is no safe mutable tag.


For any workflow that ran with a tag reference on or after 16 September 2026, rotate every secret that job could access and review what GITHUB_TOKEN was allowed to do.


Review Actions run history for Set up job failures flipping to multi-minute successes. Check runner logs for oven-sh/setup-bun or bun run $GITHUB_ACTION_PATH/index.js inside these housekeeping actions.


Audit repos for unexpected commits from 16 September onward on repositories that ran the tag references.


As an estate standard, pin every third-party Action to a full commit SHA. A platform disable is temporary containment, not a clean restore, and this incident is the proof.


How to hunt the activity


Hunt this as a chain rather than a single indicator, because every link on its own looks unremarkable: a tag compromise in May, a platform disable that was containment and never a cleanup, a 16 September re-enable with the dirty tags intact, and then an ordinary CI run quietly handing over secrets again. Before anyone escalates, name the artifact in the ticket. That means a workflow path carrying a mutable tag, a run that flipped from seconds to minutes, or a runner line showing Bun inside one of these housekeeping Actions.


Field mapping, false positives, and empty results


Field mapping: validate which fields actually hold the workflow path, the tag ref, the run duration, and the runner command lines before you trust a negative result. It is also worth confirming that you ingest GitHub audit and runner host telemetry for the repositories that matter, because plenty of organisations collect neither.


False positives: Bun turns up legitimately in plenty of other Actions, so Bun on its own proves nothing, and stray "kosche" keyword hits outside a runner context are just noise. Require either the actions-cool path or the run-duration flip before you pivot into identity review.


Empty results: an empty search may simply mean your organisation never used these two Actions, which is good news about these two Actions and says nothing about the mutable third-party tags across the rest of your estate. Missing GitHub audit or runner ingest will look exactly as clean as genuine absence. Empty means not seen in the telemetry you collected, which is a different claim from proven absent.


Hunt in KQL


// GitHub audit / CI logs: mutable tag refs to the two Actions
GitHubAuditLogs
| where TimeGenerated >= datetime(2026-09-14)
| extend raw = tostring(pack_all())
| where raw has_any (
    "actions-cool/issues-helper@",
    "actions-cool/maintain-one-comment@"
  )
| where raw matches regex @"actions-cool/(issues-helper|maintain-one-comment)@v"
| project TimeGenerated, Actor=tostring(Actor), Action, OperationName, raw
| take 500

// Runner / process evidence of Bun inside issue-housekeeping Actions
DeviceProcessEvents
| where Timestamp >= datetime(2026-09-14)
| where ProcessCommandLine has_any (
    "bun run $GITHUB_ACTION_PATH/index.js",
    "bun run",
    "oven-sh/setup-bun",
    "actions-cool/issues-helper",
    "actions-cool/maintain-one-comment"
  )
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessCommandLine
| take 500

Hunt in Falcon CQL


// Process evidence on CI runners: Bun + actions-cool housekeeping
event_simpleName=ProcessRollup2
| event_time>=2026-09-14
| CommandLine=*bun* OR CommandLine=*actions-cool* OR CommandLine=*setup-bun* OR CommandLine=*GITHUB_ACTION_PATH*
| CommandLine=/actions-cool\/(issues-helper|maintain-one-comment)/ OR CommandLine=*bun run* OR CommandLine=*oven-sh/setup-bun*
| table event_time, ComputerName, UserName, ImageFileName, CommandLine, ParentBaseFileName

// Network follow-on toward documented Mini Shai-Hulud exfil host (defanged IOC)
event_simpleName=NetworkConnectIP4
| event_time>=2026-09-14
| DomainName=*m-kosche* OR DomainName=*kosche*
| DomainName=/t\.m-kosche/
| table event_time, ComputerName, UserName, DomainName, RemoteAddressIP4, ContextProcessId

Hunt in Splunk


index=* (sourcetype=*github* OR sourcetype=*actions* OR sourcetype=*ci*)
  ("actions-cool/issues-helper@" OR "actions-cool/maintain-one-comment@")
earliest=09/14/2026:00:00:00
| eval ref=coalesce(workflow, message, _raw)
| regex ref="actions-cool/(issues-helper|maintain-one-comment)@v"
| stats count values(ref) as refs by host, user, sourcetype
| sort -count

index=* (sourcetype=*github* OR sourcetype=*actions* OR message="*Set up job*" OR message="*bun*")
earliest=09/14/2026:00:00:00
| eval duration=coalesce(run_duration, duration, elapsed)
| transaction workflow_id maxspan=1d
| search ("actions-cool" OR "issues-helper" OR "maintain-one-comment")
| where isnotnull(duration)
| table _time, workflow_id, host, duration, message

Look for jobs that failed in seconds before 16 September and succeeded in minutes afterward with no workflow-file change.


Hunt in Kibana / Elastic


// Discover: mutable tag refs and Bun inside housekeeping Actions since 2026-09-14
@timestamp >= "2026-09-14" AND (
  message: ("actions-cool/issues-helper@" OR "actions-cool/maintain-one-comment@" OR "oven-sh/setup-bun" OR "bun run $GITHUB_ACTION_PATH/index.js")
  OR process.command_line: (*actions-cool* OR *setup-bun* OR *bun\ run*)
  OR url.domain: *m-kosche*
)

// Lens: run duration flip for scheduled issue-housekeeping workflows
filters:
  - query_string: "actions-cool OR issues-helper OR maintain-one-comment OR \"Set up job\""
date histogram: @timestamp from 2026-09-14
terms: workflow.name, host.name, event.outcome
metric: median(event.duration) or median(github.run_duration)

Interpret the results before escalating


Finding

Next investigative step

Tag ref to actions-cool/issues-helper or maintain-one-comment in owned workflows

Remove or SHA-pin to a verified pre-18 May commit; inventory secrets the job can read.

Seconds-long Set up job failures flipping to multi-minute success from ~16 Sep

Treat the reopen window as exposed; rotate secrets; pull runner logs.

Bun / oven-sh/setup-bun / bun run $GITHUB_ACTION_PATH/index.js inside these actions

Preserve logs; correlate to tag resolution (a0c53dd... for @v2.2.1 per Socket).

Egress toward t.m-kosche[.]com from runners

Confirm against Socket May IoC; expand to identity and package publish review.

Unexpected commits or publishes after 16 Sep on repos that ran tag refs

Trace GITHUB_TOKEN and cloud keys those workflows held.

Empty SIEM searches

Validate GitHub audit and runner telemetry; still inventory mutable third-party Action tags estate-wide.


None of this justifies escalating on unrelated Bun usage, or on a "kosche" keyword hit outside a runner context. Require the correlated chain first.


Sources



What This Means for Your Team


This case does not open on a fresh malicious package publish, which is the shape most people are watching for. It opens when a platform disable gets treated as a cleanup, the mutable tags stay dirty underneath, and a routine scheduled job quietly starts succeeding again four months later. Containment without tag hygiene left every tag-pinned consumer exposed the moment those repositories became reachable, and nobody had to be attacked twice for that to happen. Most teams will not be able to tell you whether it happened to them, because GitHub audit logs were never forwarded anywhere, runner telemetry was never collected at all, and nobody has ever looked at the duration of a housekeeping job. So if your workflows still trust @vX.Y.Z on third-party Actions, pull the reopen window now and work out which secrets those jobs could reach, while the answer is still recoverable.


Inception Protection


Inception Protection is Inception Security's MDR on the Microsoft Defender and Sentinel stack many teams already license. For mutable Action tag reactivation we hunt workflow inventory for tag-pinned third-party Actions, run-duration flips after a disable window, Bun and GITHUB_ACTION_PATH execution inside housekeeping Actions, and follow-on identity abuse, while you pin third-party Actions to full commit SHAs and rotate secrets from any reopen-window runs.


Inception Foresight


Want to see where your environment stands against CI supply-chain reactivation that rides mutable tags after a platform disable, not just the npm package you already inventoried? Grab our free Inception Foresight. No strings. If this is useful, follow Inception Security on LinkedIn and @inceptionsec on X.

bg-map-white.png

INCEPTION SECURITY™

A cybersecurity company with in depth knowledge of the threat landscape and security controls.

NAVIGATION

GET IN TOUCH

© 2025 All Rights Reserved by INCEPTION SECURITY™ .

bottom of page