The stolen token needed no server
Wiz Research has traced Gheist, an automated campaign that takes GitHub tokens from infostealer logs and old supply-chain leaks and plants a secret-dumping workflow in every repository each token can reach: more than 13,500 branches from more than 850 accounts. The secrets leave through GitHub's own API, so there is no attacker server to block. In Bromure Agentic Coding, the GitHub token a stealer finds on a developer machine is a stand-in that GitHub rejects from anywhere else, and every write from inside the machine waits for a dialog on your Mac.
Gheist has no command server. GitHub runs the payload, GitHub writes the secrets into a log, and GitHub serves the log to whoever holds the token.
On October 25, Wiz Research published its analysis of Gheist, a campaign it has tracked since September 15. An automated tool takes a stolen GitHub token, lists every repository that token can reach, and plants a workflow that prints the repository's secrets. Wiz has recovered more than 13,500 planted branches from more than 850 compromised accounts, and as of October 24 the campaign was still running and still changing. The Hacker News covered it yesterday.
The tool needs only one input, and Wiz says where those come from: the tokens match infostealer logs and tokens exposed in earlier supply-chain compromises, such as the March compromise of the Trivy scanner. Wiz's advice to every victim includes this line: check the token owners' devices for infostealers.
How Gheist works
For each repository, 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 attributed to
the token's owner. Its API requests carry the user agent gheist, which gave
the campaign its name.
The push starts the workflow. The job prints its environment variables and
the full secrets context into the run log, both encoded in base32. Wiz
explains why that works: "GitHub masks secret values in logs only when they
appear verbatim." Base32 output doesn't match the stored values, so nothing is
masked. A matrix over the repository's deployment environments pulls in
environment-scoped secrets, which are often the production ones.
Then the tool downloads the log through the GitHub API and deletes the run seconds later. In Wiz's words: "Because the secrets leave through GitHub's own API, there is no attacker domain or outbound connection to block."
The tool kept improving. One variant added a six-minute timeout, the environment matrix and per-repository payload changes within hours of its first run on September 28. On October 19 it gained a job aimed at self-hosted runners that hunts for 31 token types, cloud instance credentials and the Kubernetes service account token.
Where GitHub's new rule stops
Since July 28, GitHub has held workflow runs it flags as malicious in public
repositories until a collaborator approves them. Every public Gheist run Wiz
checked was stuck at action_required. "The control worked, and the malicious
payloads never ran."
The hold covers public repositories on github.com only. In private
repositories and on GitHub Enterprise Server, Wiz found audit logs showing
ci-sync-* runs that completed and were then deleted, and CloudTrail records
of organization workflows assuming AWS roles from those branches.
The token comes from a developer's machine
Each Gheist branch needs a working token, and Wiz traced those tokens back to
stealer logs and leaked build tools. On a developer's laptop, the gh CLI login, a personal access token in
~/.git-credentials, a GH_TOKEN in a shell profile: an infostealer copies
all of them in one pass. Coding agents have made these tokens more common,
because an agent that pushes branches and opens pull requests needs one in
reach at all times.
The theft can come from a tool you trust. On October 19, an attacker who had held an OVHcloud developer's GitHub account for months swapped the binaries in five releases of the OVHcloud CLI, v0.11.0 through v0.15.0, for builds that, in OVHcloud CEO Octave Klaba's words, "retrieve secrets from the disk." BleepingComputer reported that the macOS, Linux and Windows builds were all affected. Klaba's advice: "If you are affected, please change all the secrets on your disk."
A stolen token doesn't have to reach a campaign like Gheist the same day. Stealer logs are traded, and a token stays useful until someone revokes it.
The same token on a Bromure machine
Bromure Agentic Coding runs Claude Code, Codex, Grok, Kimi and omp in Ubuntu VMs on your Mac. Real credentials stay on the host, and the agent works with stand-ins. Follow a GitHub token through Gheist's chain on one of those machines.
The token on disk is a stand-in
Add your GitHub token to the machine under Credentials. At boot, Bromure
writes a fake into the VM: ghp_ plus 36 characters, the shape gh expects,
in GH_TOKEN, in gh's configuration and in ~/.git-credentials. The agent's
git push and gh pr create work as before. When a request leaves the VM for
github.com, the proxy on the Mac swaps the fake for the real token, and only
for that host.
Now run the trojanized CLI inside that VM, the way an agent would after
following a project's install instructions. It reads the disk, and every
GitHub value on it is the stand-in. The same goes for the rest of the
developer's usual loot. ~/.aws/config points at a helper that hands out a
fake secret key, and the proxy re-signs AWS requests with the real one on the
Mac. The VM has no SSH private keys to read: SSH_AUTH_SOCK leads to an agent
on the host, which signs for the VM and never gives up the key.
Gheist can't use the stand-in
The Gheist tool runs on the attacker's own infrastructure. It calls
api.github.com with the token it bought and expects to see repositories.
With a Bromure stand-in, GitHub answers 401 Bad credentials. Bromure derives
the stand-in from the real token with a salt that stays on your Mac, and only
that Mac's proxy knows which real token it stands for. The tool can't list a
single repository, so it never creates a branch.
Gheist's whole method is to ask what a token can reach. From outside the VM it was written into, a Bromure stand-in reaches nothing.
A stand-in headed for a stealer trips an alarm
The stealer still has to send what it found somewhere, and from a Bromure VM
every HTTPS connection goes through the proxy on the Mac. The proxy scans each
outbound request, headers and body, for stand-ins headed to a host they weren't
minted for. A ghp_ stand-in bound for anything other than GitHub is blocked
with HTTP 451 before a byte leaves the Mac. The VM pauses, and an alert reads
This environment may have been compromised, naming the credential, the
host it belongs to and the host it was sent to.
You can shut the machine down, or save its disk and home folder for investigation. A machine marked compromised won't boot again until its disk and home are wiped, so the stealer doesn't survive the restart.
Set Unmatched traffic to Deny in the machine's Guardrails pane and allow the hosts the project needs, and the stealer's upload host is refused outright, encrypted archive or not. The refused connection is a row in the Security Timeline.
Writes from inside the machine wait for you
Suppose the malware skips the theft and uses the stand-in where it works, from inside the VM, through the proxy. It would have to do what Gheist does: create a branch and commit a workflow.
New Bromure machines set GitHub's write policy to Prompt before write.
Reads pass. A git push or a REST write to github.com pauses on a dialog on
your Mac that shows the operation verbatim:
Allow write on "GitHub" from workspace "web-app"?
POST /repos/<owner>/<repo>/git/refs
A ci-sync- branch you didn't ask for, appearing while you were looking at a
pull request, is easy to refuse. Don't allow sends the agent the same error
a blocking policy would, and Allow once grants nothing beyond that one
write.
Rotation happens in one place
Wiz's first response step is to rotate every secret an affected repository could reach and to revoke the tokens that pushed the commits. "Deleting the run or the branch doesn't undo the exposure."
On a Bromure Mac, the developer side of that rotation is one field. The real
GitHub token lives in one encrypted entry on the host. Paste the new one into
the machine's Credentials pane and the next launch writes the matching
stand-in; there are no copies to hunt down in shell profiles, .env files or
agent configs. For a key stored as a 1Password reference (op://vault/item/field
under Other API keys), Bromure re-resolves it every two minutes, so a
rotation in 1Password reaches running machines without a reboot.
The vault itself sits under a key in the macOS Data Protection Keychain, scoped to Bromure's signing identity and never synced. A stealer that copies Bromure's folder off the Mac gets ciphertext.
Let an agent run the hunt, read-only
Wiz's hunting steps are tedious work across many repositories, which makes
them good work for an agent: list every branch matching ^ci-sync-[0-9a-z]{6}$,
find the commits that add only sync-check.yml with the message
ci: sync config, pull the audit log for runs deleted within a minute of
completing and for the gheist user agent.
Give the job to a Bromure machine whose GitHub write policy is set to
Read-only. The agent can read every repository and audit log the token
allows, and any push or REST write is refused on the Mac before it reaches
GitHub. If the agent checks out a ci-sync- branch to look at it, including
one of the branches Wiz saw re-pushed with PolinRider loaders, whatever runs
does so in a VM full of stand-ins. Each credential swap shows up as a
Credential brokering row in the Security Timeline, so you can see
afterwards which hosts the hunt touched.
Stealers find stand-ins
GH_TOKEN, gh's config and ~/.git-credentials hold a ghp_ fake.
The real token stays encrypted on the Mac.
Gheist gets a 401
Off the machine, the stand-in isn't a GitHub token. The tool can't list repositories, so it plants nothing.
Leaks pause the VM
A stand-in headed anywhere but GitHub is blocked with a 451, and the VM pauses until you decide what happens to it.
Writes ask first
New machines prompt before every push and REST write to GitHub, with the exact request on screen. A hunt can run read-only.
Take the token off the laptop
GitHub's approval hold worked on public repositories, and Wiz's hardening list is the right one: required reviewers on production environments, OIDC trust scoped to protected branches, GitHub Apps or short-lived fine-grained tokens instead of personal access tokens. All of it narrows what a stolen token can do once someone holds it.
Bromure Agentic Coding works one step earlier. Your agents push branches and open pull requests all day, and the token they use never sits on a disk a stealer can read. Gheist can only plant workflows with tokens that still work, and the one on a Bromure machine works only through that machine's own proxy.
For how the same stand-ins hold up against a stealer built for coding-agent files, read The stealer packed the stand-ins. For a single stolen GitHub token that ended in a poisoned release, read The agent's token was the owner's.