← Back to Blog

The Fake Font Was After Your Agent's Keys

12 min readAtypical Tech
Illustration for The Fake Font Was After Your Agent's Keys

If you opened the wrong GitHub repo in VS Code last week and clicked Allow, a file pretending to be a font may have read your Claude Code login.

Between October 14 and 16, someone pushed the same pair of commits into 12,137 repositories belonging to 1,909 GitHub accounts, 111 of them organizations. Each pair added a hidden VS Code task and a credential stealer disguised as a font file.

OpenSourceMalware's write-up went out at 05:15 UTC on October 16, while the push was still running, and described the 94 repos and 66 owners it had analyzed. The press repeated 94. GitHub's own commit search puts the push at about 130 times that cluster.

On October 18 we checked two separate random samples of 50 of those repos. 98 of the 100 still carried the hidden task.

The count you read about was only the part someone happened to look at.

We didn't go looking for this campaign. We went looking at the forks of one popular AI tool, and it was sitting in them.


Nine bad forks in eighty thousand

REA ("Reverse Engineer Anything") is an MCP server and CLI that lets coding agents decompile and inspect binaries, Electron apps, Android packages and websites. It went from nothing to about 318,000 stars in six months, and it has more than 80,000 forks. That's a lot of copies of a tool built to be run by agents with real access.

So we crawled every fork GitHub would list: 79,887 of them. A fork can only carry new code if someone pushed to it after creating it, which left 4,921. Comparing each of those with upstream left 373 forks with commits of their own.

We then fetched every one's .vscode/tasks.json. Upstream REA has no .vscode folder at all, so any task file in a fork was added there.

Nine forks had a task set to run hidden, the moment the folder opens, pointing node at a "font":

  • sooogooo/rea
  • nkukehenry/rea
  • captainyyds/rea
  • cyberspace-cs/rea
  • sagar-dalwala999/rea
  • Edwin77-Axaz/rea
  • ElonMuskOfficial/rea
  • yaseenammaar/rea
  • zhaikong/rea

On the afternoon of October 18 all nine were still up, task file and all. We've left them as plain text on purpose. Don't clone them, and don't open them in an editor to "take a look."

They come from two different campaigns. Four carry GhostAction, a year-old credential-stealing campaign GitGuardian first documented; OpenSourceMalware traced this cluster's first stage to October 8. Five carry a variant of PolinRider, the fake-font technique OpenSourceMalware described in September. In one of them the payload sits in a test-fixture folder seven folders deep, posing as an icon font.


REA was collateral

Nothing we found says anyone picked REA. The four GhostAction owners had their accounts taken over, and the push went into everything they owned. The five PolinRider-style forks match how OpenSourceMalware describes that campaign: an infected developer machine pushing with its own cached Git credentials.

One GhostAction owner had 125 of its 126 public repos hit, its REA fork among them. We spot-checked four repos from the GhostAction owners that have nothing to do with REA, and all four carry the same task file. Every one of the nine forks existed at least a day before its malicious push, so these are ordinary forks that got caught, not repos made for the attack. The largest single victim in the campaign data is a template-generator account with 931 repos hit, and a sibling account has 181 more.

That's why we describe these owners as hijacked, not malicious. Their repos are the evidence. They're also the victims.

A fork inherits its owner's compromise, not its upstream's reputation.

It's also why star counts are the wrong comfort. Popular tools weren't targeted here. Their forks just end up inside the blast radius of whoever made them.


What the font file wants

The launcher is ordinary VS Code configuration. A task labeled eslint-check, marked "hide": true and "reveal": "never", runs node ./public/fonts/fa-solid-400.woff2 with "runOn": "folderOpen". The "font" is 38 lines of plain Node.js.

Here's what it reads:

  • ~/.aws/credentials, .npmrc, .pypirc, .git-credentials, .netrc, .kube/config
  • ~/.claude/.credentials.json, Claude Code's login
  • ~/.gemini/oauth_creds.json, the Gemini CLI's login
  • SSH keys

Then it searches your home directory for Anthropic, AWS, GitHub, npm and PyPI token patterns. It dumps environment variables whose names start with ANTHROPIC, OPENAI, AWS_, HF_TOKEN or GITHUB_TOKEN, among others. It posts the lot to a server on port 8080. Finally it installs a cron job that fetches and runs whatever that server sends, every five minutes.

The GhostAction forks also carry GitHub Actions workflows. The ones aimed at self-hosted runners, the build machines you host yourself, steal cloud instance credentials, read OpenAI's auth.json and up to eight .env files, and grep the whole machine for AI keys: sk-ant-, sk-proj-, sk-or-v1-, xai-, gsk_, AIza.

Read that list again. Your coding agent's credentials now sit on the same list as your AWS keys.

The stealer doesn't care what your agent writes; it wants the keys your agent runs on.

A stolen Claude Code login or Anthropic key lets someone else run an agent as you, on your bill, with whatever your account can reach.

That's the case for keys small enough to lose, which we made in Your Agent Doesn't Need OAuth. It Needs a Smaller Key. A key scoped to one job, short-lived, with its own spend cap, turns this from a breach into a bad afternoon.


The defaults did the defending

Two guardrails stood between these forks and their victims, and nobody had to configure either one.

VS Code asks first. Automatic tasks are off by default (task.allowAutomaticTasks), VS Code asks once per workspace, and per its own documentation, automatic tasks "never run in an untrusted workspace, regardless of this setting."

So a victim has to trust the folder and then click Allow. That's a real barrier, but a thin one: anyone who clones a fork to actually work on it is likely to do both.

The PolinRider forks go further and ship a .vscode/settings.json that tries to switch automatic tasks on and hide the terminal. We haven't tested whether VS Code honors that from a workspace file. We wrote about this whole class, a trusted folder whose files change underneath you, in You Trusted the Folder. The File Changed.

GitHub held the workflows. Across the four GhostAction forks there were 66 malicious workflow runs. Every one of them was held at action_required, waiting for an approval that never came. REA's own CI ran normally on the same pushes.

Some of the held runs were started by hand from the owner's account, which suggests the attacker tried to push them through. We can't tell you exactly why GitHub held them. We can tell you the secret-harvesting stage never ran in these forks.

Defaults did the defending here, and nobody chose them.

The case defaults won't save is a self-hosted runner attached to a public repo. OpenSourceMalware saw the self-hosted variant in one repo. It's in all four GhostAction REA forks.


A cheap tell for forged commits

The PolinRider-style forks don't just add files. Their newest commit is a rewritten copy of a real upstream REA commit, keeping the original author, message and timestamps, so a glance at the log looks normal.

Here's what gives it away. Upstream's squash-merged commits are signed by GitHub: verified, with GitHub as the committer. The forged copy has the same author, message and date, but it's unsigned, and its committer is the author. An unsigned commit that duplicates a GitHub-signed upstream commit is a check anyone can script.

A forged commit can copy everything except the signature.

Commit history is easy to fake when nobody checks who actually committed. We've argued before that the history often can't tell you who wrote the bug. Signatures are the one part a rewrite can't carry over.


Check yourself in five minutes

On every machine where you write code:

  • On Mac or Linux, run crontab -l | grep ghc2. Any hit means the stealer ran. (One variant has a Windows branch whose persistence we haven't established.)
  • Search your clones for a .vscode/tasks.json with "runOn": "folderOpen" and "hide": true, especially one pointing node at a font or any other file that isn't code.
  • Look for "task.allowAutomaticTasks" set in a repo's .vscode/settings.json, node api.js added to the front of npm scripts, and .gitignore lines naming temp_auto_push.bat.

If it ran:

  • Treat the machine as compromised. The cron job runs whatever its server sends, so deleting the line doesn't undo what already ran. Take the machine off the network and plan to rebuild it.
  • From a clean machine, rotate everything in the list above: cloud keys, Git and SSH credentials, npm and PyPI tokens, and your AI-tool logins and API keys.
  • Check your own repos for "Add editor task" and "Add font assets" commits, a new .vscode/tasks.json or a new public/fonts/ folder. If a GitHub token was taken, the push may have gone into your repos too. Remove the files, revoke your GitHub tokens and sessions, and tell anyone who cloned.

Going forward:

  • Keep task.allowAutomaticTasks off, and open clones you haven't reviewed in Restricted Mode.
  • Don't attach self-hosted runners to public repos.
  • If you use REA, install rea-agents from npm or clone morluto/rea directly.

What we know, and what we don't

Known. GhostAction's collection server and its phishing domain share one IP address, which OpenSourceMalware rates as confirmed. The fake-font task technique comes from PolinRider, which OpenSourceMalware calls DPRK-linked. It calls GhostAction a copycat, found no indicators shared with other DPRK malware, and names no group. Neither do we.

Inferred, not established. The same IP spent the first week of September, by GreyNoise's count, scanning the internet for exposed .git/config files. Tokens harvested that way could explain how these accounts were taken over, but nothing ties a specific token to a specific push. The commits also carry their committers' local timezones, which fits pushes made from the victims' own machines. We haven't verified that either.

Unknown. Who runs this. How each account was first compromised. Whether the GhostAction and PolinRider-style forks share an operator; their files and servers differ.


Indicators of compromise

Network: 193.32.204.199 (AS153622). Port 8080 is used for collection and for the /c2/<hostname> command channel. Exfiltration tags: ?c=auto_20261014, ?c=auto_20261015, ?c=reinject, ?c=p16n.

Host:

  • A crontab line ending in #ghc2.
  • A VS Code task labeled eslint-check that runs node on a file under public/fonts/.
  • A DEVBOX header on outbound reports.

GhostAction files (SHA-256):

FileSeen inSHA-256
fa-solid-400.woff2 (tag auto_20261015)sooogooo, cyberspace-cs61eca87b4b4c3e6248dc0fde25ff9cf28637798a8c4be172eaef9843252d12f6
fa-solid-400.woff2 (tag reinject)nkukehenrycb3842832267421b9563c7bd32d3582a8fff624860e57c2c9058966735d4da60
fa-solid-400.woff2 (Windows-capable, new)captainyydsf293f3d0baabcd091fb4d5a424bbdcd6f3f9262a828f4a0f14d80dc5aea345fb
tasks.jsonsooogooo, nkukehenry, cyberspace-csa043f03e89ef55779f4f630138cf5efe1dd37632e176c7a0d9bbfa989ba84eeb
tasks.json (Windows branch, new)captainyyds5b474948ec8f1c76cc60750882d4755248b00f99852d245660a849b684dd5836
cleanup-job.yml (self-hosted)sooogooo94b88c669302b2c4b1de7333603c5c342a77bcb9aeb91857b904d17f21a4a2df
code-review.ymlsooogooob8a61a1fef136ca2e079a5b3d5510b107bfaf8b7b742810466adc8758bebbf3e
maintenance.yml (self-hosted, p16n)sooogooo8b7e9aec581dd2b78ed33c81916e057452675d71c00a1a1a5e9af34890f8883a
test-suite.yml (p16n)sooogooo13ac87a0b4b929b309a7e5efe84675ba6d5d713875aa768d28075a40688645a8
audit.ymlnkukehenry5c6acdd07d5f0cf3b99d450f5c80fe7360853e0833bf948144f24c6c443dc474
cache-cleanup.yml (self-hosted)nkukehenry6b01fe8761c87b12a385337e84f5ad5590b2f7f3006199ba22e09c19f8d2168f
security-audit.ymlnkukehenryb9ea5a0e645ff7c62642cb96a9118f3192645762302d60058571cd31713b04b2
lint.yml (p16n)captainyyds18f4f42ff6ebd706e364d241b6d8a993c76bf3f91dc90f73c0ff1fec94ff93fa
maintenance.yml (self-hosted)captainyyds67df64b1a1a1d8b6641120cfb88133121ac76722a98ee354cfce1a220e411117
runner-maintenance.yml (self-hosted, p16n)captainyydsabd7ffa78265f5e7a1fde00590fc386fb8a25dd2cde7251562d0ab131edcd88f
test-suite.ymlcaptainyyds9cd5488efa74a67559c7a1c6de8e3d03f54fdf3139635ad3daf0cd64a46ee423

PolinRider-style files (SHA-256):

FileSeen inSHA-256
fa-solid.lkfsagar-dalwala999, Edwin77-Axaz3ae470cabb9412bf5f0e64e1f71b458e14eb10b19070845597d5b8230b27d552
fa-solid-700.otfElonMuskOfficialca92a5d5286319008106d15de85dbb58d9160c6b8f02375dedbbf9be097427d6
fa-solid-700.otfyaseenammaar25dc037b1ffb01e0373e2a06a4c6ad6eade18c69b8730e6f54eff2790a5956c7
fa-brands-400.ttf (nested in a test fixture)zhaikong1fd44e006879a1fc04dea6e5ff372646f0600f632c99e18cfa6e19c0b4c27181
tasks.jsonsagar-dalwala999, Edwin77-Axaz1db793e883ae13501ce00b1d793e11c749795864d7bc35ae203d2cac92ae3097
tasks.jsonElonMuskOfficial, yaseenammaaref2f957b4684053bd4abc4c6054f1c2ed322ab41f4a83c856705feb4bfcf3ed4
tasks.jsonzhaikong8d1d2071f946fe1debdc3e1f2920bfb328d8fdec334a5c7960a274bc35908120
api.js (padded with 315 leading tabs)sagar-dalwala999, Edwin77-Axaz41a69ba3946396d2d1c15f2e43be705f0793e8773dee0f3dda89a541d31c00d9

How we counted

We crawled all 810 pages of REA's fork list, compared every fork pushed after creation against upstream, and fetched the task file for the 373 forks with their own commits, plus 27 GitHub couldn't diff. We sized the campaign with commit search for the campaign's commit message, sliced into time windows small enough to stay under GitHub's 1,000-result cap.

The same message appears 0 to 4 times a day from October 1 to 13. Then it jumps to 1,548 on October 14, 7,950 on October 15 and 2,674 on October 16, and falls back to zero on October 17. A second message, "Add font assets," tracks it almost exactly: the two are paired commits from one automated run.

Every sample was read as text through GitHub's API. Nothing was run, and nothing touched the attacker's server. All numbers are as of October 18.

Limits.

  • Commit search mostly leaves out forks. Only 22 of the 12,137 repos are forks, so fork infections are undercounted.
  • "Still infected" means the task file was on the default branch on October 18. It doesn't mean anyone ran it.
  • The counts drift down as owners clean up. A recount on the afternoon of October 18 found 7,900 commits for October 15, 50 fewer than that morning.
  • Nobody outside GitHub can count downloads. Clone numbers are visible only to repo owners, and all nine forks have zero stars, zero watchers and zero forks of their own.

Sources: OpenSourceMalware's GhostAction copycat post (Oct 16), its GhostAction phishing post (Oct 11) and its PolinRider post (Sep 26). VS Code's tasks documentation. GitHub's REST API (forks, compare, contents, Actions runs, commit and code search), all read October 18.

We reported the nine forks to GitHub.

The campaign didn't need a zero-day. It needed a stolen login, a folder someone trusted, and one click on Allow. Your agent's keys are worth stealing now. Make them small enough that losing one is a bad day, not a breach.

Related Posts