Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches
An automated tool is using stolen GitHub tokens to plant ci-sync branches whose workflow prints every secret base32-encoded, grabs the log and deletes the run. How to hunt for it across an org, what to rotate, and the controls that stop the next one.
Key takeaways
- Since at least September 15, 2026, the Gheist tool has used stolen GitHub tokens to plant more than 13,500 ci-sync branches across 850+ accounts.
- Each runs a workflow that prints every secret base32-encoded, downloads the log and deletes the run.
- Private and GitHub Enterprise Server repositories are exposed; hunt for the branches, then rotate every reachable secret.
On this page
If any token in your GitHub org could have been stolen, search every repository for ci-sync- branches today. An automated tool Wiz calls Gheist pushes a workflow that prints all secrets base32-encoded, so log masking misses them, then downloads the log and deletes the run. Private and GitHub Enterprise Server repositories are the exposed ones.
Verified against
Campaign details, indicators and dates against Wiz Research's Gheist report (October 25, 2026); masking, OIDC subject and audit log behavior against GitHub's Secure use reference, OpenID Connect reference and organization audit log events docs. Checked November 8, 2026.
What happened, in dates
Wiz Research has tracked the activity since September 15, 2026 and published its report on October 25. By then it had recovered more than 13,500 planted branches from more than 850 compromised accounts, from personal projects to private enterprise repositories. The name comes from the gheist user agent the tool sends with its API calls. Where Wiz could trace the tokens, they came from infostealer logs and from tokens exposed in earlier supply-chain compromises such as the March Trivy compromise.
The pattern is the same everywhere. For each repository a token can reach, the tool creates a branch named ci-sync- plus six random characters and commits one file, .github/workflows/sync-check.yml, with the message ci: sync config. The commit is unsigned and authored as the token's owner. The push triggers the workflow, which sets permissions: {} (that limits GITHUB_TOKEN, not the secrets context) and pipes both env and toJSON(secrets) through base32 -w 120. Where a repository has deployment environments, one variant adds a job matrix that runs once per environment, so environment-scoped production secrets print too.
The tool then pulls the run log through the GitHub API and, in Wiz's words, "deletes the run seconds later." There is no command-and-control server: the secrets leave through GitHub itself, so there is no domain or IP to block.
Two dates change the risk picture. Since July 28, 2026, GitHub holds potentially malicious workflow runs in public repositories until a collaborator approves them, and every public Gheist run Wiz checked sat at action_required. That hold does not cover private repositories or GitHub Enterprise Server, where audit logs showed runs completing and being deleted. On October 19 the tool changed its payload, and hours later a sync-host job with runs-on: self-hosted appeared on repositories that had a self-hosted runner registered. It searches the host for 31 token formats, private keys, .env files and Terraform state, reads /proc/*/environ, mounts the host filesystem through the Docker socket, and asks for cloud instance credentials and the Kubernetes service account token. Wiz found the job buggy, for now.
Am I affected? Hunt across the org
Wiz gives the branch pattern as ^ci-sync-[0-9a-z]{6}$ (two variants run in parallel: one uses hex suffixes, the other, first seen September 28, base36; the regex covers both). The first loop uses the matching-refs endpoint, one prefix query per repository instead of paging through every branch:
# 1. ci-sync branches in every repo of an org (empty repos return 409, hidden by 2>/dev/null)
ORG=your-org
for r in $(gh repo list "$ORG" --limit 5000 --json nameWithOwner -q '.[].nameWithOwner'); do
> gh api "repos/$r/git/matching-refs/heads/ci-sync-" -q '.[].ref' 2>/dev/null \
> | sed 's|^refs/heads/||' | grep -E '^ci-sync-[0-9a-z]{6}$' | sed "s|^|$r: |"
> done
# 2. In a local clone: the commit message, and Wiz's published workflow blob SHAs
git fetch --all --prune
git log --all --oneline --grep='^ci: sync config$'
git log --all --oneline --find-object=f3da2b8b357dcda02fd455d314be2a95007516a6
# 3. Deleted runs and the gheist user agent (org owner, GitHub Enterprise Cloud, read:audit_log)
gh api "orgs/$ORG/audit-log?phrase=action:workflows.delete_workflow_run&per_page=100" --paginate \
> -q '.[] | [(.["@timestamp"]/1000 | todate), .actor, .repo, .user_agent] | @tsv' | grep -i gheist
# 4. AWS: OIDC role assumptions from a ci-sync branch (90 days, current region only)
aws cloudtrail lookup-events --start-time 2026-09-15 \
> --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
> --query 'Events[].CloudTrailEvent' --output text | grep -o 'ci-sync-[0-9a-z]\{6\}' | sort | uniq -c
The blob SHA in check 2 is the plain variant; Wiz lists three more (d09535857a741bbb976bdc084a9a021713133291, 1dc5af9cce950aad1eb1546ecce8565b117540c4, c665b778b2d822453293003e7ca0bbb2cb08e39d), and the environment-matrix files differ per repository, so the commit message search matters more. Rerun check 4 in every region your runners call STS in.
| Indicator | Value | Where to look |
|---|---|---|
| Branch | ci-sync- plus 6 characters | Branches API, local clones |
| Commit | ci: sync config, only sync-check.yml | git log --all |
| Audit log | workflows.delete_workflow_run, user agent gheist | Org audit log |
| Runner files | .gdmp_pat, .gdmp_patg, .gdmp_sweep, gdmp-pivot | Self-hosted runner hosts, job logs |
What to do now
Found a branch? Revoke the token that pushed the commit (the author is the token's owner) and check that person's machines for an infostealer. Then rotate every repository, organization and environment secret the repository could read. The workflows.prepared_workflow_job audit event lists the secrets provided to each job, which tells you exactly what one run received. Deleting the branch or the run does not undo anything: the attacker already has the log.
Check the cloud side. Wiz saw the same enterprise organization's cloud credentials used hours later to enumerate AWS Secrets Manager, and CloudTrail showing workflows on attacker branches assuming AWS roles. Treat any role a ci-sync run assumed as used.
Self-hosted runners. If a ci-sync branch ran on one, treat the host and its instance role as compromised, and rebuild it rather than clean it.
Public repositories. Do not approve a held sync-check run. That approval is the only thing between the payload and your secrets.
Where this breaks
Log masking was never a control against someone who can edit a workflow. GitHub's Secure use reference says "automatic redaction is not guaranteed" and that anyone with write access can read every secret in the repository, so a stolen token with push rights is the whole attack.
The OIDC check has a blind spot. GitHub's default subject is repo:ORG/REPO:ref:refs/heads/BRANCH only when the job has no environment; a job that references an environment gets repo:ORG/REPO:environment:NAME instead, and the branch never appears. Gheist's environment matrix produces exactly those jobs, so for environment-bound roles, correlate role assumptions by time against the deleted runs from check 3. Repositories created, renamed, transferred or opted in after July 15, 2026 use an ID-based subject (repo:OWNER@OWNER-ID/REPO@REPO-ID:...); the branch grep still matches, but trust policies must match it.
The audit log API needs GitHub Enterprise Cloud, the log keeps 180 days, and the REST endpoint defaults to three months without a created: qualifier. Wiz also does not report whether the self-hosted variant ran successfully in any private repository, so absence of evidence is not a clean bill for runner hosts.
Questions people ask
Does GitHub Actions log masking protect my secrets?
Only from accidents. Masking replaces a secret's exact value in log output; base32, base64 or any other transform produces a different string that prints in full. GitHub's own guidance says redaction is not guaranteed and that a transformed value must be registered separately. Anyone who can push a workflow file can read the secrets, which is why the defense is limiting who can push workflows, not masking.
Does deleting the workflow run remove the leaked secrets?
No. Gheist downloads the log before it deletes the run, so by the time the run disappears the encoded secrets are already off GitHub. Deleting the ci-sync branch or the run removes evidence, not exposure. Rotate every secret the repository could read, including organization and environment secrets, and revoke the token that pushed the commit.
Are public GitHub repositories safe from Gheist?
Mostly, so far. Since July 28, 2026, GitHub holds potentially malicious workflow runs in public repositories until a collaborator approves them, and every public Gheist run Wiz checked was stuck at action_required. The risk is someone approving one. Private repositories and GitHub Enterprise Server have no such hold, which is where Wiz saw runs complete.
The decision, concretely
- Using classic personal access tokens for automation? Move to GitHub Apps or short-lived fine-grained tokens with an expiry; a stolen token that expires in days limits the window.
- Can anyone with write access change
.github/workflows/? On private and internal repositories, a push ruleset with Restrict file paths on.github/workflows/**blocks those pushes for everyone outside the bypass list. - Production secrets in environments? Add required reviewers and limit deployment branches to selected branches, since environment secrets are released only after those rules pass. On private repositories, required reviewers need GitHub Enterprise.
- OIDC roles trusting
repo:ORG/*? Pin the subject toref:refs/heads/mainor a named environment, as covered in our OIDC federation guide for AWS.
The call we'd make
Run the four hunt checks today on every org you own, and treat one hit as a full secret rotation for that repository, not a branch cleanup. Then close the door: a push ruleset on workflow paths and environment reviewers stop this tool even with a valid stolen token. The caveat: Gheist is evolving and the self-hosted variant goes after the runner host, so the GitHub Actions hardening steps from the Trivy compromise matter as much as the hunt.
Sources
- Wiz Research: Gheist, attempted scaled abuse of stolen GitHub tokens stumbles
- GitHub Changelog: GitHub Actions holds potentially malicious workflows for approval (July 28, 2026)
- GitHub Docs: Secure use reference
- GitHub Docs: OpenID Connect reference
- GitHub Docs: Audit log events for your organization
Cite this article
Kiril Urbonas (2026). Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches. DevOpsNess. https://www.devopsness.com/blog/github-actions-gheist-stolen-tokens-ci-sync-secrets
[Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches](https://www.devopsness.com/blog/github-actions-gheist-stolen-tokens-ci-sync-secrets) (Kiril Urbonas, DevOpsNess, 2026-11-08)
More from DevOps
Explore more articles in this category
Argo CD CVE-2026-113708: Read-Only Users Can Run Code on the Repo-Server. Upgrade to 3.4.10+
Three CVSS 9.9 Argo CD repo-server bugs can be reached by anyone with Application get, the permission most teams hand to every viewer. Fixed versions, the v2 and 3.3 support cliff, and the RBAC and config checks to run today.
CVE-2026-111497 "Broccoli2Shell": One Brotli Bug in Node.js, Envoy and Istio. Patch Node First
An out-of-bounds write in Google's Brotli decoder, now on CISA's KEV list, reaches Node.js through zlib and Envoy and Istio through the Brotli decompressor filter. Fixed versions, cluster-wide checks and what to block until you patch.
kaniko CVE-2026-105769: Lookalike ACR Hosts Get Your Azure Token. Rebuild on v1.25.20
A FROM line pointing at a host like x.azurecr.io.attacker.com is enough to make kaniko hand the build's Entra ID token to that host. Which kaniko forks and images are affected, how to find Azure identities in your build pods, and what to rotate.
You might have missed
Evergreen posts worth revisiting.