Gheist is an automated campaign that uses stolen GitHub tokens to push a sync-check workflow onto ci-sync- branches, print every reachable secret into the Actions log in base32, download the log and delete the run. Private repositories and GitHub Enterprise Server are most exposed. Hunt for the branches, rotate reachable secrets and revoke the tokens.
A security vendor's research team published its analysis on 25 October 2026. It reports more than 13,500 planted branches recovered from more than 850 compromised accounts, ranging from personal projects to enterprise private repositories, and says the campaign was still running and changing as of 24 October. There is no CVE here. Nothing in GitHub was broken. The attacker holds valid tokens and uses ordinary platform features, which is exactly why the usual perimeter controls have nothing to block.
The good news, and it is real, is that a GitHub control shipped on 28 July 2026 stopped the public repository side of this campaign cold. The bad news is that the control does not cover private repositories or GitHub Enterprise Server, and that is where the researchers saw secrets leave, cloud roles get assumed and AWS Secrets Manager get enumerated. This post covers what happened, the indicators you can hunt for today, the exact commands to check your organisation, and the changes that make a stolen token far less useful next time.
What happened
The campaign is named after the user agent string, gheist, that its tooling sends on GitHub API requests. According to the research team, the operator feeds in GitHub tokens harvested from infostealer logs and from earlier supply chain compromises, with the March 2026 Trivy compromise cited as one source. For each repository a token can reach, the tool creates a branch, commits a single workflow file, lets the push event start the workflow, pulls the run log through the API and deletes the run seconds later.
Timeline
| Date (2026) | Event | Source |
|---|---|---|
| 19 to 21 March | Trivy ecosystem compromise; the official advisory says all secrets reachable from affected pipelines must be treated as exposed. The research team later cites Trivy as one source of Gheist's tokens | Trivy advisory GHSA-69fq-xp46-6x23; research blog |
| May | Megalodon campaign backdoors GitHub repositories through CI workflows. 57 accounts from Gheist's first phase were also Megalodon victims | Research blog; public incident tracker |
| 15 July | Per GitHub's documentation, repositories created after this date use an immutable default OIDC subject format that includes owner and repository IDs | GitHub OIDC docs |
| 28 July | GitHub starts holding potentially malicious workflow runs in public repositories on github.com until a collaborator with write access approves them | GitHub changelog |
| 15 September | The research team starts tracking Gheist | Research blog |
| 28 September | First run of the second tool variant ("Tool B") | Research blog |
| 6 October | 391 accounts rename about 1,461 repositories to cracked software lures; the renames wipe 610 ci-sync branches. 166 of those accounts never appear in Gheist activity | Research blog |
| 18 October | 25 Gheist accounts each create a "Windows RDP Runner" repository within nine minutes, abusing Actions for free compute | Research blog |
| 19 October | Tool B adds a base32 separator line to its output, then hours later pushes a second sync-host job aimed at self-hosted runners | Research blog |
| 19 and 20 October | Three victims already infected with PolinRider re-push Gheist commits with a loader appended to vite.config.js | Research blog |
| 24 October | Campaign still active and evolving; roughly two thirds of implicated accounts no longer available on GitHub. The team has reported the activity to GitHub | Research blog |
| 25 October | Public write-up and indicators published | Research blog |
The 6 and 18 October events and the PolinRider re-pushes happened on the same compromised accounts, but the research team presents them as other activity on those accounts. Whether one operator runs all of it is not established in the published material, so treat the overlap as shared victims and timing, not proven common control.
What GitHub has said
GitHub has not, as far as we could find, published a statement specific to Gheist. The research write-up contains no direct quote from GitHub and says only that the activity was reported to GitHub. What GitHub has published is the control that blunted the campaign on public repositories.
The GitHub changelog entry dated 28 July 2026 says Actions now holds "certain workflow runs for approval before they start." According to that entry:
- The protection "currently applies to public repositories on github.com only."
- GitHub Enterprise Server does not have it yet.
- A held run does not execute until a repository collaborator with write access approves it, and the approval must come from an authenticated web session.
- No configuration is required; GitHub applies it automatically.
- The changelog does not describe how a run gets flagged.
- The background GitHub gives is recent supply chain attacks that used compromised credentials to push workflows that steal CI/CD credentials, which is the Gheist pattern exactly.
The research team reports that every public Gheist run it checked, across every variant including the self-hosted job, sat at the action_required status and never ran. That is a clear win for a default-on control, and it is the reason the write-up's title describes the campaign as stumbling.
GitHub's secrets documentation is also relevant. It recommends ::add-mask:: for sensitive values that are not stored as secrets, warns that base64 "is not a substitute for actual encryption," and notes that secrets are not passed to workflows triggered from forks. The page does not promise that log redaction catches transformed or encoded values, and Gheist relies on exactly that gap.
Who is affected
| Situation | Exposure | Why |
|---|---|---|
| Public repository on github.com, token stolen | Branch and workflow file planted; run held at action_required | The 28 July approval hold applies. Do not approve the run |
| Private or internal repository on github.com, token stolen | Secrets very likely dumped | The hold does not apply. Runs completed and were deleted, per the research team's audit log evidence |
| GitHub Enterprise Server, token stolen | Secrets very likely dumped | The hold is not available on GHES |
| Repository with deployment environments | Environment secrets exposed too, unless environment protection blocked the job | Tool B lists environments in a job matrix so each job can read environment-scoped secrets |
| Repository with a registered self-hosted runner | Runner host, its files, process environments and instance role at risk | The sync-host job runs only where a self-hosted runner exists |
| Workflows that use OIDC to reach a cloud provider | Cloud role assumed from the attacker's branch if the trust policy allows any branch | The research team saw organisations' own workflows assume AWS roles on ci-sync-* branches |
| Account owners whose token was stolen | Device may be infected by an infostealer | Infostealer logs are one of the token sources |
If your organisation has never had a token appear in an infostealer log, a compromised CI tool or a leaked configuration, you are probably not in the 850 accounts. But you cannot know that from the outside, and the hunting steps below are quick.
How it works
This section explains the mechanism in plain language so you can reason about your own exposure. It does not reproduce the workflow.
- A valid token. The operator starts with a GitHub token that belongs to a real user or automation account. The token needs permission to push code and to create or change workflow files. Classic personal access tokens need the
workflowscope for that; fine-grained tokens need the Workflows permission. - A throwaway branch. For every repository the token can reach, the tool creates a branch named
ci-sync-plus six random characters. - One file, one commit. It commits
.github/workflows/sync-check.ymlwith the messageci: sync config. The commit is unsigned and authored as the token owner, so at a glance it looks like that person pushed a routine CI change. - The push is the trigger. The workflow runs
on: push, so creating the branch is enough to start it. Branch protection onmaindoes not matter; this is a new branch with its own copy of the workflow. - Secrets into the log, encoded. The job prints its environment and then the whole
secretscontext, base32 encoded at 120 columns. GitHub redacts secrets from logs when they appear verbatim. A base32 encoded value is a different string, so it prints in the clear. - Permissions do not help. The workflow even declares
permissions: {}. That setting limits the automaticGITHUB_TOKEN, which the attacker does not need. It does nothing to the secrets context. - Environments widen the haul. Where a repository has deployment environments, Tool B builds a matrix with one job per environment, so environment-scoped secrets (often production credentials) are read as well, unless a protection rule stops the job.
- Collect and clean up. The tool downloads the run log through the GitHub API and deletes the run seconds later. Every request carries the
gheistuser agent. - No outbound traffic. The secrets never leave the runner over the network. They go into GitHub's own log storage, and the operator reads them back through GitHub's API. There is no command and control domain, so egress filtering and DNS blocklists see nothing.
In the enterprise cases the team examined, the tool cycled through tokens belonging to several users and automation accounts within minutes, cloned private repositories before planting the workflow, and hours later used cloud credentials from the same organisation to enumerate AWS Secrets Manager.
Technical details
Everything in this section comes from the research team's published indicators and from GitHub's own documentation. Use it for hunting and detection only.
Indicators of compromise
| Type | Value |
|---|---|
| API user agent | gheist |
| Branch name pattern | ^ci-sync-[0-9a-z]{6}$ |
| Tool A branch suffix | Six hexadecimal characters |
| Tool B branch suffix | Six base36 characters |
| Commit message | ci: sync config |
| Files changed | Only .github/workflows/sync-check.yml |
| Commit signature | Unsigned, authored as the token owner |
| Workflow name | sync-check |
| Payload string | toJSON(secrets) }}' | base32 -w 120 |
| Separator output (Tool B, from 19 October) | Prints LAFA====, likely for the operator's log parser |
| Self-hosted job log header | ===== gdmp-pivot: followed by the stage name |
| Self-hosted helper files | .gdmp_pat, .gdmp_patg, .gdmp_sweep |
Workflow blob SHAs
Git stores each file version as a blob with a SHA-1 hash, so identical files have identical blob IDs across every repository. The research team published four:
| Variant | Blob SHA |
|---|---|
| Plain | f3da2b8b357dcda02fd455d314be2a95007516a6 |
With timeout-minutes: 6 | d09535857a741bbb976bdc084a9a021713133291 |
| With base32 separator | 1dc5af9cce950aad1eb1546ecce8565b117540c4 |
With self-hosted sync-host job | c665b778b2d822453293003e7ca0bbb2cb08e39d |
The environment matrix variants differ per repository, because the matrix lists that repository's own environment names, so they have no fixed hash. Do not rely on blob SHAs alone; combine them with the branch pattern and commit message.
Workflow structure
| Field | Value | Notes |
|---|---|---|
name | sync-check | Same in all variants |
| Trigger | on: [push] | The branch creation starts it |
Top-level permissions | Empty map | Restricts GITHUB_TOKEN only |
| Job | sync on ubuntu-latest | GitHub-hosted runner |
timeout-minutes | 6 | Added by Tool B within hours of its first run |
| Matrix | One entry per deployment environment | Tool B, where environments exist |
| Second job | sync-host on self-hosted | Tool B, from 19 October, only where a self-hosted runner is registered |
Tool A and Tool B
Both variants ran in parallel. Tool A uses a hex branch suffix and the plain payload. Tool B uses a base36 suffix, changes the payload per repository, added the timeout and environment matrix soon after 28 September, added the separator on 19 October and then the self-hosted job. The branch suffix alone tells you which tool hit a repository.
The sync-host job on self-hosted runners
According to the research team, the self-hosted job writes the three .gdmp_* helper files and logs each stage under a gdmp-pivot header. Its stages:
- Host details and crontab.
- A recursive search of the home directory, the workspace and system paths for 31 token formats (including AI provider keys), private keys,
.envfiles, Terraform state and shell history. - The environment of every process, read from
/proc. - If the Docker socket is reachable, a container with the host root filesystem mounted, and the same search repeated on the host.
- AWS instance role credentials, plus GCP and Azure metadata.
- GitHub Actions and GitLab runner credential files.
- The Kubernetes service account token.
The team notes three bugs: the job looks for the wrong runner key file, its URL credential regular expression is broken, and it does not actually collect GCP or Azure tokens. Do not take comfort from those bugs. The tool changed several times in October and a fixed version could already exist. If a ci-sync branch ran on one of your self-hosted runners, treat the host and its instance role as compromised.
OIDC subjects to search for
When a GitHub workflow requests an OIDC token for a cloud provider, the sub claim names the repository and either the branch or the environment. GitHub's documentation shows forms like these:
repo:octo-org/octo-repo:ref:refs/heads/main
repo:octo-org/octo-repo:environment:prod
A role assumption from a Gheist branch would carry a branch subject such as repo:your-org/your-repo:ref:refs/heads/ci-sync-a1b2c3. If your trust policy matches repo:your-org/your-repo:*, that assumption succeeds. Repositories created after 15 July 2026 use an immutable default subject format with owner and repository IDs, per GitHub's documentation, so search for the ci-sync- substring rather than an exact string.
What this is not
There is no CVE and nothing for the CISA Known Exploited Vulnerabilities catalogue, because no GitHub vulnerability is involved. It is also distinct from GhostAction, the 2025 campaign that pushed a workflow posting secrets to an external endpoint. The research team says Gheist uses different workflow files and, unlike GhostAction, sends nothing to an attacker host.
Related campaigns and what actually links them
Four other names appear in the research. The links differ in strength, and it helps to keep them straight.
| Campaign | Relationship to Gheist | Strength of link |
|---|---|---|
| Trivy compromise (March 2026, GHSA-69fq-xp46-6x23, CVE-2026-33634) | Cited as one source of the tokens Gheist uses | Source of credentials, per the research team |
| Megalodon (May 2026) | 57 Gheist first-phase accounts were also Megalodon victims | Shared victims |
| PolinRider | Three PolinRider-infected victims re-pushed Gheist commits with a loader in vite.config.js on 19 and 20 October | Shared victims; a community research repository attributes PolinRider to DPRK-linked actors |
| GhostAction (2025) | Similar idea, different workflows and exfiltration | Distinct campaign |
The Trivy advisory is worth rereading in this light. It lists trivy 0.69.4 (and Docker Hub images 0.69.5 and 0.69.6) as affected with 0.69.2 and 0.69.3 as safe, trivy-action below 0.35.0 as affected with 0.35.0 patched, and setup-trivy below 0.2.6 as affected with 0.2.6 patched. It says all secrets reachable from affected pipelines "must be treated as exposed and rotated immediately." If a GitHub token sat in one of those pipelines and was never rotated, Gheist is the kind of campaign that eventually uses it. Credential rotation after a supply chain incident is not optional cleanup; it is the step that decides whether the next campaign works.
Megalodon, as described by a public incident tracker, injected workflows into more than 5,500 repositories within hours in May 2026, using forged CI-style author names and base64 encoded payloads, and it affected npm releases built from poisoned repositories. PolinRider, per its community research repository, appends obfuscated JavaScript to build configuration files and rewrites git history with back-dated, force-pushed commits. Neither is the same tool as Gheist. What they share is the target: repositories and the CI systems that hold their secrets. We covered a related workflow-based secrets campaign in GhostAction returns, and the general response pattern in our compromised dependency playbook.
What to do now
Run these steps in order. They assume the GitHub CLI (gh) is installed and authenticated as an organisation owner or someone with admin rights on the repositories, plus jq, git and, for cloud checks, the AWS CLI.
1. Find planted branches across your organisation
ORG=your-org
gh repo list "$ORG" --limit 2000 --json nameWithOwner -q '.[].nameWithOwner' |
while read -r repo; do
gh api "repos/$repo/branches" --paginate -q '.[].name' 2>/dev/null |
grep -E '^ci-sync-[0-9a-z]{6}$' | sed "s|^|$repo |"
done | tee gheist-branches.txt
An empty file is good, but not proof. The operator, or a lure rename, may have deleted branches. Step 3 covers that.
2. Confirm each hit against the indicators
git clone --no-checkout "https://github.com/$ORG/REPO.git" && cd REPO
git fetch origin '+refs/heads/ci-sync-*:refs/remotes/origin/ci-sync-*'
for b in $(git branch -r --list 'origin/ci-sync-*'); do
echo "== $b"
git ls-tree "$b" .github/workflows/sync-check.yml # blob SHA
git log -1 --format='%H | %an | %ae | sig=%G? | %s' "$b"
git diff --stat "origin/HEAD...$b" # should show one file
done
Compare the blob SHA with the four published hashes. A match on hash, the ci: sync config message, sig=N (unsigned) and a single changed file is a confirmed plant. A non-matching hash with everything else matching is probably an environment matrix variant. Note the author: that account's token was stolen.
3. Check held and completed runs
# Runs GitHub held for approval. Do NOT approve these.
gh run list --repo "$ORG/REPO" --status action_required \
--json databaseId,headBranch,workflowName,createdAt
# Any surviving runs on ci-sync branches
gh run list --repo "$ORG/REPO" --workflow sync-check --limit 100 \
--json databaseId,headBranch,status,conclusion,createdAt
Runs that completed and were deleted will not appear here. For those, use the audit log. On GitHub Enterprise Cloud the organisation audit log API supports phrase search:
gh api --paginate \
"orgs/$ORG/audit-log?phrase=action:workflows.delete_workflow_run&per_page=100" \
| jq -c '.[] | select(tostring | test("ci-sync-|gheist"; "i"))'
gh api --paginate \
"orgs/$ORG/audit-log?phrase=action:workflows.completed_workflow_run&per_page=100" \
| jq -c '.[] | select(tostring | test("ci-sync-"))'
The pattern the researchers describe is a run on a ci-sync-* branch completing and being deleted within a minute. Which events carry a user agent field depends on your plan and on whether you use audit log streaming; if you stream to a SIEM, search there for gheist as well. On GitHub Enterprise Server, run the same searches in the site admin audit log or in the log store you forward to.
4. Check your cloud audit logs
If any workflow uses OIDC to reach AWS, look for role assumptions whose subject mentions ci-sync-. Run this in each region where you call STS, or against your organisation trail in your log store:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
--start-time 2026-09-01T00:00:00Z \
--query 'Events[].CloudTrailEvent' --output json \
| jq -r '.[] | fromjson | select(tostring | test("ci-sync-"))
| [.eventTime, .requestParameters.roleArn, .responseElements.subjectFromWebIdentityToken] | @tsv'
For each hit, take the assumed role session and search for what it did next, especially ListSecrets and GetSecretValue against Secrets Manager, which is the follow-on activity the researchers saw:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=ListSecrets \
--start-time 2026-09-01T00:00:00Z --query 'Events[].CloudTrailEvent' --output json \
| jq -r '.[] | fromjson | [.eventTime, .userIdentity.arn, .sourceIPAddress] | @tsv'
This cloud signal is independent of GitHub. The research team points out that it can reveal Gheist even where GitHub's own logs are gone. Apply the same idea to Azure and Google Cloud workload identity federation logs if you use them.
5. Check self-hosted runners
On every self-hosted runner host registered to an affected repository or organisation:
sudo find / -xdev \( -name '.gdmp_pat' -o -name '.gdmp_patg' -o -name '.gdmp_sweep' \) 2>/dev/null
sudo grep -rl 'gdmp-pivot' /path/to/actions-runner/_diag 2>/dev/null
Also check the runner's job history in GitHub for any job on a ci-sync-* branch. A single match means the host, its local files, any secrets in process environments, the Docker host if the socket was reachable, and the instance role are all compromised. Rebuild the host from a clean image. Rotating credentials without rebuilding leaves whatever else ran there.
6. List every secret the affected repository could read
Deleting the branch or the run does not undo exposure. The research team is explicit about this. Build the rotation list:
REPO="$ORG/REPO"
gh secret list --repo "$REPO"
for env in $(gh api "repos/$REPO/environments" -q '.environments[].name'); do
echo "== environment: $env"
gh secret list --repo "$REPO" --env "$env"
done
gh secret list --org "$ORG" # check visibility: all, private, or selected repositories
For organisation secrets with selected visibility, check whether the affected repository is in the list:
gh api "orgs/$ORG/actions/secrets/SECRET_NAME/repositories" -q '.repositories[].full_name'
Repeat for Dependabot and Codespaces secrets if you use them, and for variables that hold anything sensitive. For doing this across a whole organisation rather than one repository at a time, see how to map your GitHub Actions secrets blast radius.
7. Rotate, then revoke, then clean up
- Rotate each secret at its source (the cloud console, the registry, the SaaS provider), then update the GitHub secret. Rotating only the GitHub copy changes nothing.
- Revoke the token that pushed the commit. The owner can revoke it under their developer settings. On Enterprise Cloud with SAML single sign-on, organisation owners can list and remove credential authorisations with
gh api orgs/$ORG/credential-authorizationsand aDELETEon the credential ID. - Review fine-grained token access to the organisation with
gh api orgs/$ORG/personal-access-tokensand revoke anything unexpected. - Check the token owner's machine for an infostealer before issuing a new token, or the new one will leak the same way.
- Delete the
ci-sync-*branches only after you have recorded hashes, authors and timestamps for the incident record. - Never approve a held
sync-checkrun and never run code from aci-syncbranch.
8. Harden before the next token leaks
Environment protection is the single change most likely to stop this pattern, because the secrets that matter most can live behind it. Require reviewers and restrict deployments to protected branches:
gh api -X PUT "repos/$ORG/REPO/environments/production" --input - <<'JSON'
{
"reviewers": [{ "type": "Team", "id": 1234567 }],
"deployment_branch_policy": { "protected_branches": true, "custom_branch_policies": false }
}
JSON
With that in place, a job on a ci-sync-* branch that names the production environment waits for a reviewer, or is rejected because the branch is not protected, before any environment secret is released.
Then scope cloud trust policies to the environment or a protected branch instead of any ref in the repository. An AWS example:
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"
}
}
}
Finally, move automation from classic personal access tokens to GitHub Apps or short-lived fine-grained tokens without the Workflows permission unless they truly need it, and run self-hosted runners as ephemeral hosts without Docker socket access or open instance metadata.
What changed since first reports
The public write-up is one day old, but the tooling it describes changed repeatedly over six weeks:
- 28 September: Tool B appears. Within hours it adds the six minute timeout and the environment matrix, which is the change that reaches production secrets.
- 6 October: a lure rename wave on 391 accounts wipes 610
ci-syncbranches as a side effect, which means branch searches alone undercount. - 19 October: Tool B adds the base32 separator and then the self-hosted job, moving from secrets in the secrets context to everything on the runner host.
- 24 October: about two thirds of implicated accounts are gone from GitHub, which the researchers attribute to response activity. The campaign is still active.
Also new since July: GitHub's approval hold for public repositories. Without it, the public side of this campaign would likely have looked very different.
Open questions
- How many tokens and organisations in total? The research team gives branch and account counts, not token or organisation counts. No victims have been named, and we do not name any.
- Will GitHub extend the approval hold? The changelog says the protection "currently" covers public github.com repositories only. There is no published date for private repositories or GHES.
- Who runs the lure and cryptomining activity? The research ties it to the same accounts, not provably to the same operator.
- Has the self-hosted job been fixed? The three bugs were present in the version analysed. A later version may not have them.
- How were the original tokens stolen in each case? Infostealers and earlier compromises are named sources, but per-victim attribution is not public.
What would have helped
Practices any team can use
Treat write access as secret access. Anyone, or any token, that can push a workflow file to a repository can read every repository and organisation secret that repository can see. Inventory who holds those tokens and which ones carry workflow permissions. Most automation does not need to edit workflows.
Put production secrets behind environments with reviewers. Repository and organisation secrets are readable by any workflow on any branch. Environment secrets can be gated by required reviewers and protected-branch deployment policies. Gheist's matrix trick only works where those gates are missing.
Scope OIDC trust to environments or protected branches. A trust policy that accepts any ref from a repository lets a throwaway branch assume your deploy role. Match on the environment subject instead.
Prefer short-lived credentials. GitHub Apps installation tokens expire in an hour. Fine-grained personal access tokens can be scoped to a few repositories with an expiry. A classic token found in an infostealer log months later is still valid if nobody set an expiry.
Rotate after every upstream CI incident. When a tool your pipelines run is compromised, as Trivy was in March, the advisory's instruction to rotate everything reachable is the step that prevents a second incident.
Make self-hosted runners ephemeral. One job per fresh host, no Docker socket, no unrestricted metadata access and a minimal instance role. The sync-host job is built to exploit long-lived, well-provisioned runners.
Alert on the boring signals. A workflow run deleted within a minute of completing. A new branch whose only change is a new workflow file. A role assumption whose subject names a branch nobody recognises. None needs a vendor feed.
Protect developer machines. Infostealer logs were a token source. Endpoint protection, browser credential hygiene and short token lifetimes all shrink the value of a stolen session.
Where Safeguard fits
Safeguard does not see GitHub tokens or audit logs, and it would not have stopped a valid token from pushing a branch. The useful role is upstream and downstream of the incident.
- Inventory. Safeguard builds an SBOM inventory of your repositories and container images (
safeguard scan --dir .,--image <name>,--sbom <file>), and the platform shows which projects contain a given component and version. After a compromise of a tool that your builds pull in as a dependency or ship inside an image, that query tells you which projects carried the affected version, and so which pipelines' secrets belong on the rotation list. Our post on finding affected products fast with an SBOM walks through that query. - Policies and gates. Safeguard policies and gates are evaluated in the platform and can run in CI through
safeguard exec, so a build that carries a known vulnerable component version can fail while you rotate. - Remediation. Autonomous remediation opens tested pull requests that move affected dependencies to safe versions, in bulk across projects. It ships switched off, with every category set to Manual, so your team decides what runs on its own.
- Hardening work with an assistant. The Safeguard CLI's assistant can edit workflow files, tighten
permissionsblocks, pin actions and rewrite OIDC trust policies in your infrastructure code. It asks before any command that changes something, and pushes nothing unless you tell it to. - Incident evidence. The inventory gives you a dated record of what each affected project contained when the incident happened, which is what auditors and customers ask for afterwards.
Where Safeguard does not help: revoking tokens, rotating secrets at their source, reading GitHub or CloudTrail audit logs, enabling environment protection and cleaning infostealers off laptops. Those are GitHub, cloud and endpoint tasks, and the steps above cover them.
Zero-Day Discovery and Gold
Safeguard's Zero-Day Discovery looks for previously unknown flaws in open source packages before a CVE exists. Gheist involves no package flaw at all; it is credential abuse of a working platform, so Zero-Day Discovery is not the relevant tool here and we make no claim that it played a part. Gold Open Source at gold.safeguard.sh is free and needs no login; it gives CVE intelligence with affected and fixed versions, CVSS, EPSS and KEV status. Gheist itself has no CVE, so there is nothing to look up for it. Its practical use is on the CI tools and dependencies whose compromise puts tokens in circulation: checking affected and fixed versions quickly when the next advisory lands, alongside the vendor's own advisory. The habit that matters is the same as for any supply chain incident: pull fixed components from a source you trust, rebuild quickly and rotate what the old pipeline could reach.
If you want to try the CLI on your own workflows, download it from safeguard.sh/download and read the CLI docs.
Common mistakes
- Deleting the branch and calling it done. The secrets were printed and downloaded seconds after the push. Deleting the branch or run removes evidence, not exposure.
- Rotating only the GitHub secret. Updating the value in GitHub without revoking the old key at the cloud provider or registry leaves the stolen value valid.
- Approving a held run to see what it does. The 28 July hold is the only thing between a public repository and the dump. A collaborator clicking approve finishes the attack.
- Trusting
permissions: {}in your own workflows as a secrets control. It limitsGITHUB_TOKENonly. Secrets are governed by where they are stored and by environment rules. - Searching only the default branch. The planted file lives on a
ci-sync-*branch. Code search onmainfinds nothing. - Forgetting renamed repositories. The lure rename wave wiped branches. Check audit logs, not just current branches.
- Rotating before revoking the token. If the stolen token still works, the operator can push again and read the new secrets. Revoke first, then rotate, or do both in the same window.
- Leaving the self-hosted runner in place. Rotating its instance role without rebuilding the host leaves files and processes you have not inspected.
- Missing environment and organisation secrets. The rotation list is everything the affected repository could read, not just its own repository secrets.
FAQ
Is Gheist a GitHub vulnerability?
No. There is no CVE and no flaw in GitHub. The operator uses stolen tokens and normal Actions features. GitHub's role so far is the 28 July approval hold, which stopped the public repository runs the researchers checked.
Why did GitHub's secret masking not stop it?
GitHub redacts secrets from logs when they appear verbatim. The workflow base32 encodes the whole secrets context, so the printed text no longer matches any secret value and is not redacted. GitHub's documentation does not promise that encoded or transformed values are caught.
Our repositories are private. Are we safer?
Less safe for this campaign. The approval hold covers public repositories on github.com only, so private repositories ran the workflow, and the researchers saw secrets dumped, roles assumed and Secrets Manager enumerated in private and enterprise cases.
We run GitHub Enterprise Server. Does the hold apply?
No. The changelog says GHES does not have the protection yet. Rely on environment protection, token hygiene and audit log monitoring.
Does permissions: {} in a workflow protect secrets?
No. That setting limits the automatic GITHUB_TOKEN. The secrets context is still available to the job. The attacker's workflow uses the same setting.
We found a held sync-check run at action_required. What now?
Do not approve it. Record the branch, commit and author, delete the branch, revoke the author's token and check their device. Because the run never executed, the secrets were not dumped through that run, but the token is still stolen and may have reached private repositories elsewhere.
What is the single best defensive change?
Move production credentials into environments with required reviewers and protected-branch deployment rules, and scope cloud OIDC trust policies to those environments. That turns an arbitrary branch from a full secrets dump into a job waiting for approval.
Is this connected to the Trivy compromise?
The research team cites the March 2026 Trivy compromise as one source of the tokens Gheist uses. Trivy's own advisory told users to rotate all secrets reachable from affected pipelines. Unrotated tokens from that incident are exactly what a campaign like this consumes.
Is Gheist the same as GhostAction?
No. The researchers say Gheist uses different workflow files and exfiltrates through GitHub's own API, while GhostAction sent secrets to an external endpoint.
How do we know if our cloud account was touched?
Search cloud audit logs for OIDC role assumptions whose subject contains ci-sync-, then follow those sessions. In AWS, look at AssumeRoleWithWebIdentity and then ListSecrets and GetSecretValue events.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
For Gheist specifically, the "upstream" is your own CI configuration and your credentials, so the remediation that matters is rotation and hardening; the flows above apply to the CI tools and dependencies, such as the Trivy components from March, whose compromise put tokens in circulation in the first place.
See how Safeguard would have helped
None of these demos shows Gheist, GitHub audit logs or secret rotation. Each shows a real flow that maps onto the hardening work after this incident.
The first demo is a real Safeguard CLI session asked to fix a lodash CVE: it reads the finding, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and pushes nothing. After Gheist you would ask the same assistant to add environment references to your deploy jobs, tighten each workflow's permissions block and bump trivy-action to 0.35.0 or later in every workflow that uses it, then review the diff before anything leaves your machine.
Click through it at your own pace, or open it full screen.
The second demo is a real desktop app session: the CLI session shows in the conversation list, you ask for work on your computer, pick the folder, then Approve or Deny each command and file edit, and it ends with an upgrade done and tests passing. For this incident, point it at the repository that holds your cloud IAM code and ask it to change the GitHub OIDC trust condition from any ref to the production environment subject, approving each edit.
Click through it at your own pace, or open it full screen.
The third demo is a real Android session: you send work to a computer running the CLI and approve the command on the phone with Allow once, Allow for turn or Deny. During an incident like this one, when the on-call engineer is away from a desk, that is how a workflow hardening change can still move with a human approving each step.
Click through it at your own pace, or open it full screen.
Quick check
Does deleting a ci-sync branch and its workflow run undo the exposure?
No. The secrets were printed to the log and downloaded through the API seconds after the push. Rotate every repository, organisation and environment secret the affected repository could read, and revoke the token that pushed the commit.
Which repositories did GitHub's 28 July approval hold protect?
Public repositories on github.com. Private repositories and GitHub Enterprise Server are not covered, and that is where the researchers saw secrets dumped and cloud roles assumed.
Sources
- Gheist: Attempted Scaled Abuse of Stolen GitHub Tokens Stumbles (security vendor research blog, 25 Oct 2026)
- GitHub Actions holds potentially malicious workflows for approval (GitHub changelog, 28 Jul 2026)
- Using secrets in GitHub Actions (GitHub Docs)
- About security hardening with OpenID Connect (GitHub Docs)
- Trivy ecosystem supply chain temporarily compromised, GHSA-69fq-xp46-6x23 (Aqua Security, GitHub advisory)
- CVE-2026-33634 (NVD)
- Megalodon campaign backdoors GitHub repositories via CI workflow compromise (incident tracker)
- PolinRider (community research repository)
- GhostAction campaign (incident tracker)
- GhostAction returns: the security audit workflow (Safeguard blog)
- Map your GitHub Actions secrets blast radius across an org (Safeguard blog)
- SBOM incident response: finding affected products fast (Safeguard blog)
- Incident response playbook for a compromised dependency (Safeguard blog)
Last reviewed: 26 October 2026. This is a developing story; check the linked advisories for the latest status.