Security12 minJune 2026

Understanding & Removing theGit Config Injection Worm

How a self-propagating Git worm spreads across local project folders — detection, removal, and a Windows-first playbook.

By Sharon Rosario · June 2026

Malware AnalysisGitWindowsDevSecOps
Published
June-August 2026 (3 recurrences)
Campaign
Git config injection worm
Scope
75 repos audited, 173 fixes
Outcome
Removed, verified & guarded
Platform
Windows (scripts included)

Breathe

First things first: you are going to be okay

If you are reading this because you found strange, obfuscated code in your configuration files or weird background tasks running in VS Code, take a deep breath. Your project is not permanently ruined, and you are not alone.

Discovering malware inside your own codebase is a uniquely terrifying experience. You suddenly feel like you can't trust your own tools, your repositories, or even your recent commits. I know exactly how that feels because I had to deal with this exact threat spreading through my own workspace. It can feel deeply violating and incredibly stressful.

I wrote this guide to walk you through exactly what happened, how to find the hidden traces, and most importantly, how to safely remove the malware and secure your repositories again. We are going to tackle this step by step. You don't need to panic, you don't need to format your hard drive, and you certainly don't need to delete all your hard work.

By the end of this guide, you will have removed the malware, verified the result directly against the remote, and put controls in place that make reinfection much harder. Let's get through this together.

Key takeaway

"This is a scary situation, but it is entirely fixable. I have been in your exact shoes, and we are going to walk through the solution together, step by step."

What Happened

Understanding the silent intruder

To defeat a threat, you have to understand how it thinks. This malware is clever, but it follows a highly predictable pattern that we can easily trace.

The malware you've encountered is what security researchers call a self-propagating Git worm. Unlike traditional viruses that try to infect your entire operating system or encrypt your files, this worm specifically targets software developers. It looks for Node.js and JavaScript project folders, treating your workspace like a playground.

What makes it so tricky is that you don't even have to download a malicious package or clone an infected repository to get hit. If a colleague's machine is infected and they share a network drive or sync folder with you, the worm can quietly crawl over to your projects. It moves laterally across your filesystem, jumping from one project folder to the next.

Once it finds its way into your project, it waits patiently. The moment you open that folder in Visual Studio Code, it springs into action. It doesn't ask for permission; it uses automated VS Code tasks to quietly run in the background. From there, it searches for configuration files—like your `postcss.config.js` or `vite.config.js`—and hides its malicious code far out of sight.

Finally, it commits these changes to your Git history and pushes them to GitHub — and this is where it is at its most deceptive. It does not add an obvious new commit. Instead it rewrites the commit already sitting at the tip of your branch, keeping your original commit message and your original author date exactly as they were. Only the committer identity and its timezone offset change. Because GitHub displays and orders history by the author date, the rewritten commit stays precisely where the original sat and looks entirely untouched. Nothing new surfaces in your activity feed, because as far as the feed is concerned, nothing new happened.

There is one place the truth is recorded, though: the GitHub Events API. Git itself does not store push times, and the commit dates are inherited from whatever commit was rewritten -- so both are useless for dating the attack. But /users/USERNAME/events returns real push timestamps. Mine showed 112 pushes to 42 repositories in 2 minutes 42 seconds, roughly one per second, walking the repository list in strict alphabetical order. That is a script iterating an API listing, not a worm crawling folders.

That same listing showed something I had not expected: two of the repositories hit were not mine. They belonged to collaborators who had given me write access. The infection did not just spread across my account -- it reached outward through my permissions into other people's projects, and they had no idea.

Working out where it originally came from took longer, and the answer was in the shared client repositories. The worm ships two committed files as its dropper, and git stores every object it has ever seen, so I could simply ask each repo whether those exact blobs existed in its object store. They did -- in two repositories I share with my team. Combined with the cleanup commits already in their history from 10 and 13 June, weeks before my own first cleanup on 17 June, the sequence became clear: the dropper was committed into a shared repository, I pulled it, and opening that folder in VS Code was enough. Nothing else was needed.

Why your usual antivirus probably missed this

Standard security tools like Malwarebytes, Windows Defender, and CrowdStrike Falcon are exceptionally good at detecting what they were designed to detect: malicious executables, suspicious process injection, and known malware signatures. This worm deliberately sidesteps all of that. It never creates a malicious .exe. It uses Node.js — a completely legitimate runtime already installed on your system — to execute its payload. It uses VS Code tasks — a built-in, trusted feature — as its trigger. Because every individual piece it uses is legitimate developer tooling, conventional antivirus has no heuristic rule to flag it. The only effective scanner for this threat is the manual audit described in this guide.

The Attack Chain

How it slipped past your defenses

Let's break down exactly what the worm does from the moment you open VS Code. Knowing its exact moves takes away its power and gives you back control.

Phase 1

Phase One: The Local Trigger

It uses VS Code's trust against you.

  • It begins by modifying your `.vscode/settings.json` file to set `"task.allowAutomaticTasks": true`. This forces VS Code to run tasks without asking for your explicit permission.
  • It registers a malicious task in `.vscode/tasks.json` that triggers the absolute second you open the folder. No manual terminal commands needed.
  • The task usually executes a file disguised as a web font, such as `fa-solid-400.woff2`. However, this is not a font file at all—it is a highly obfuscated Node.js script acting as the primary payload.

The Malicious Setting

{
  "task.allowAutomaticTasks": true
}
Phase 2

Phase Two: The Great Cover-Up

Hiding in plain sight within your config files.

  • The active payload frantically scans your local directory for common, trusted configuration files. It specifically targets `postcss.config.js`, `eslint.config.js`, `tailwind.config.js`, and `vite.config.js`.
  • Instead of replacing your code (which would break your app and alert you immediately), it gently appends its own obfuscated JavaScript to the very end of the file.
  • To ensure you don't notice it during casual edits, it prepends hundreds of blank spaces before its code. If you have word-wrap turned off in your editor, you can look right at the file and never see the danger hiding completely off-screen.

The Obfuscation Signature

global['!']='8-3997-1';var _$_1e42=(function...
Phase 3

Phase Three: Ghost Commits

Rewriting history in place, so that nothing ever looks new.

  • Once the files are infected, the worm rewrites the commit already at the tip of your branch to carry the payload, rather than adding a visible new one. It then force-pushes the result.
  • It preserves your original `GIT_AUTHOR_DATE` exactly. Across the repositories examined for this write-up, the author timestamp on the rewritten commit was byte-identical to the original — only the committer name (shortened to your GitHub display name) and the committer timezone offset (`-0700`, GitHub's own server timezone) were changed.
  • Because GitHub renders and sorts history by author date, the rewritten commit keeps its original position and reads as unmodified — your commit message is still yours, and the date beside it is still correct. The real tell is that the commit SHA changed while the message and author date did not, which is also what silently triggers a fresh Vercel or CI deployment.
  • Finally, it adds its own push scripts (like `temp_auto_push.bat`) to your `.gitignore` file so they never trigger a warning when you run `git status`.
Phase 4

Phase Four: Server-side propagation

Why it reaches repositories you never opened.

  • With a harvested token, the attacker does not need your machine at all -- repositories are rewritten through the GitHub API directly. That is why it is so fast: dozens of repos in minutes, in parallel.
  • The proof in my case was unambiguous: 23 of my infected repositories had no local copy on my machine. I had to clone them fresh to even scan them. A worm crawling my filesystem cannot touch a repository that is not on my filesystem.
  • The commit forensics agree. The committer name on the malicious commits was my GitHub PROFILE display name ("Sharon"), not my configured git user.name ("Sharon Rosario") -- a local git client does not know your profile name, but the GitHub servers do. The timezone was the GitHub server timezone (-0700) rather than mine (+0530).
  • The commits were UNSIGNED, which rules out browser-session/cookie theft: commits created through the GitHub web UI are auto-signed with the GitHub web-flow key and display as "Verified". So this was a token used against the API, not a hijacked browser session.
  • This is also why it hit every team member: each compromised token grants access to whatever that person can write to. A single stolen token is enough to poison a shared repository for everybody.
Phase 5

Phase Five: Branch flattening and lost work

The force-push collapses your branches -- this is where people lose changes.

  • The push is not a normal push. It is a force-push (git push --force), which does not add to history -- it REPLACES it. Whatever the branch pointed at before is simply discarded on the remote.
  • In at least one of my repos the force-push left multiple branches pointing at the SAME commit: staging had been reset onto the main/landing-page history. Around two months of feature commits on staging vanished from the remote, and the staging deployment started serving the same thing as production. That is the symptom a client is most likely to report: staging and live suddenly look identical.
  • The critical thing to understand: the loss is on the REMOTE only. Force-push cannot touch your local clone. Your local branch still holds the real history -- so if you have a local copy, nothing is actually lost, it just needs pushing back.
  • How to spot it: for each branch compare local against the remote. `git rev-list --count <branch>` versus the remote count, or `git status` showing your branch is "ahead" of origin by many commits, means your local has work the remote lost. Two different branches whose remotes point at the same commit is the other tell.
  • How to recover it safely: back up first (copy the repo folder, or `git bundle create backup.bundle --all`), confirm your local branch tip is payload-clean, then restore the remote from local with `git push --force-with-lease origin <branch>`. Use --force-with-lease, not --force: it aborts if the remote changed unexpectedly. Pushing the branch will also re-trigger the deployment, which puts the correct app back on the staging/live URL.
  • If you have NO local copy of a flattened branch, the old commits still exist on the server as unreachable objects for a while -- open a GitHub Support request quickly, as they can sometimes recover a branch from its reflog before it is garbage-collected.
Phase 6

Phase Six: Reconstructing it afterwards

The four checks that actually produced answers.

  • PUSH TIMES -- git does not record when a push happened, and commit dates are inherited from whatever commit was rewritten, so neither can date the attack. The GitHub Events API can: GET /users/USERNAME/events returns real push timestamps for up to 300 recent events. Mine showed 112 pushes across 42 repositories in 2 minutes 42 seconds, about one per second, in strict alphabetical order -- machine speed, API-driven, iterating a sorted list.
  • BLAST RADIUS -- that same listing showed two repositories in the burst that I did not own. They belonged to collaborators who had granted me write access, so a compromised token reached outward into their projects. Read the repo field of every push event, not only your own namespace, and warn anyone whose repository appears.
  • ENTRY POINT -- git keeps every object it has ever seen, so you can ask a repository whether a specific blob was ever present with git cat-file -e SHA. The dropper ships as two fixed files, so their blob SHAs are stable fingerprints. Run that against your shared team repos: mine came back positive on two of them, which proved the dropper had been committed there and that I had pulled it. Then git log --all --find-object=SHA surfaced the surrounding cleanup commits, dated 10 and 13 June, weeks before my own first cleanup on 17 June.
  • WHAT DID NOT WORK -- git log -S on .gitignore looked promising for dating the infection and produced confident dates going back to 2025. Those dates are meaningless, because the worm rewrote existing commits and preserved their original dates. The giveaway was that every result carried a -0700 or -0800 timezone (GitHub server) while my genuine commits are +0530. If a forensic result looks surprisingly old, check the timezone before believing it.
  • PERSISTENCE -- worth checking so you can stop worrying about your machine. I found none: HKCU and HKLM Run/RunOnce keys were all legitimate, the Startup folders held only desktop.ini, the single non-Microsoft scheduled task was one I wrote myself (and configured so a missed run cannot fire on wake), and all 15 editor extensions were mainstream with no markers. This worm needs no OS-level persistence, because the dropper lives in your repositories instead.

Key takeaway

"Because it relies on VS Code automatic tasks and off-screen spaces, you wouldn't have noticed it through normal development habits. Please remember: it is not your fault it slipped by."

Detection

Let's check your system

The fastest single check takes ten seconds: open a root config such as postcss.config.js, press End or scroll fully right. If the line ends where you expect, that file is fine; if a wall of code is hiding out there, you have found it. That check is necessary but not sufficient -- below is the full list, including the indicators that let this survive two previous cleanups.

File paths & triggers

  • .vscode/tasks.json (committed INTO the repo)

    The real persistence mechanism, and the reason this recurs. Because it is committed, it travels with the code to every clone. Look for an innocuously named task -- mine said "eslint-check" -- whose command is `node ./public/fonts/fa-solid-400.woff2`, with "hide": true, "reveal": "never" and "runOn": "folderOpen". Across 12 of my repos this file was byte-identical (blob 5e226620).

  • .vscode/settings.json (committed INTO the repo)

    Ships with the task and sets "task.allowAutomaticTasks": true so it runs without prompting. Also byte-identical across repos (blob 934d5554). The tell: a Python scraper and a Node backend having the same settings.json means it is a dropped template, not your config.

  • public/fonts/fa-solid-400.woff2

    The payload the task executes. 9,129 bytes, and its first four bytes are spaces (0x20202020) instead of the wOF2 font magic. Real FontAwesome has no "solid-400" weight -- it is solid-900 -- so the filename itself is fabricated. Check magic bytes, never filenames.

  • Source files, not only configs

    Where config-only scanners fail. I found the payload appended to src/routes/auth.js, src/routes/tools.js, routes/AdminRoutes.js and routes/admin/index.js. My own scanner reported CLEAN on all of them because it only inspected *.config.js. Scan every tracked text file.

  • .gitignore

    Three appended lines -- branch_structure.json, temp_auto_push.bat, temp_interactive_push.bat -- so the push-bot artifacts never appear in git status. Present on 68 of my branches, wider than the payload itself, and the easiest indicator to miss because the file still looks normal.

  • temp_auto_push.bat / temp_interactive_push.bat / branch_structure.json

    The artifacts those .gitignore lines conceal. In my incident these were never on disk at all -- only the .gitignore entries were -- so treat the entries as the primary indicator.

  • ~/.node_modules

    A hidden directory in your user home where some variants stage networking libraries. Absent in my case; check anyway, it costs one command.

String signatures

  • A long whitespace run, then global.i= or global.r=require

    The highest-signal indicator, and better than the markers alone. The payload is appended after 50+ spaces so it sits off-screen to the right. Searching for the PADDING plus the bootstrap has a near-zero false-positive rate; searching for global[...] alone also flags anything that merely documents the worm -- including this case study.

  • global.i="A8-3997-1"

    A campaign identifier. Every infected file across all 75 of my repositories carried the same value, which is how I could tell this was one actor hitting everything rather than several unrelated infections.

  • global['!'] / global['_V'] / global['_t_t']

    Obfuscation entry points from earlier variants. Still worth grepping, but expect false positives on security notes and scanner scripts.

  • Unicode-escaped requires

    The payload loads modules as require("\u0068\u0074\u0074\u0070") -- that is "http" -- plus https, zlib, url and child_process. The escaping defeats naive greps for child_process, which is another reason to search for the padding instead.

  • import { createRequire } from 'module'

    PREPENDED to ESM configs (vite.config.js, *.mjs) so the CommonJS-style payload can call require(). I found this orphaned shim in 38 files. If a config imports createRequire but never calls require(), the shim is injected -- remove it too, or you leave evidence of tampering in place.

  • A trailing semicolon that was not yours

    The worm inserts ; before its padding so the concatenation stays syntactically valid. Subtle, but it means naive truncation leaves a stray semicolon and your file will not be byte-identical to the original. Restore from git history instead.

  • trongrid.io / bsc-dataseed / 166.88.54.158

    Blockchain RPC endpoints and the known C2 address, found in the payload body. Useful for confirmation, not for discovery.

Quick audit script

PowerShell: 60-Second Project Scan

# Run from the root of any project folder. Prints any infected config files.
Get-ChildItem -Recurse -Include postcss.config.js,next.config.js,vite.config.js,tailwind.config.js,eslint.config.js |
ForEach-Object {
  $content = Get-Content $_.FullName -Raw
  if ($content -match "global\['!'\]|global\['_V'\]|_\$_1e42") {
    Write-Host "[INFECTED] $($_.FullName)" -ForegroundColor Red
  } else {
    Write-Host "[CLEAN]    $($_.FullName)" -ForegroundColor Green
  }
}

A scanner that finds nothing looks identical to a clean machine

Every round of this incident my local scanner reported CLEAN, and it was telling the truth about my laptop -- which genuinely was never infected. The infection lived in my GitHub repositories, and a local scanner structurally cannot see that. I also hit three bugs that produced FALSE CLEAN results. One: in Git Bash, git show ref:path/with/slash is silently mangled by MSYS path conversion and returns EMPTY, so a check "passed" by reading nothing -- that is how worm entries on 68 branches were reported clean. Two: git update-index --force-remove needs a work tree, so in bare clones ten repos reported success while deleting nothing. Three: a cleanup script that greps for worm signatures will corrupt any file that DOCUMENTS them -- an earlier pass stripped the code sample out of this very post and silently broke my site build for eleven days. Before trusting any check, run it against a known-infected sample and confirm it fails.

Removal Playbook

Taking your repository back

We are going to do this in a very specific, deliberate order: stop it from running, clean your files, fix your Git history, and secure your accounts. Take it one step at a time, and you will be fine.

A note on platforms and scripts

I wrote these cleanup scripts natively for Windows because that is where I first encountered the threat. `clean_git_malware.ps1` runs in PowerShell, and `worm-guard.sh` runs via Git Bash. If you are on a Mac or Linux machine, you can safely copy the contents of these scripts into an AI tool like ChatGPT or Claude and simply ask: "Convert this malware cleanup script to a native bash script for my OS, keeping the exact same scan logic and file signatures." It will work perfectly.

1

Step 1: Kill execution first, globally

Set this in your GLOBAL user settings, not a workspace. It is the single change that stops the dropper from auto-running again -- including in repos you have not cleaned yet and repos you clone tomorrow. Do it before anything else, because later steps involve pulling potentially infected code.

VS Code / Cursor User Settings

"task.allowAutomaticTasks": "off"
2

Step 2: Check your machine, then stop worrying about it

Mine was clean every single time. Scan for the payload padding, fake fonts, .vscode autorun tasks and ~/.node_modules -- then move on. The decisive evidence: 23 of my infected repositories had NO local copy on my machine, and I had to clone them fresh to even scan them. A filesystem-crawling worm cannot infect a repository that does not exist on disk. That one fact proves the spread was server-side via the GitHub API with a stolen token, not folder crawling.

Command

bash worm-guard.sh /path/to/your/projects
Download script
3

Step 3: Revoke the credential actually in use -- the OAuth grant, not just PATs

The step I got wrong first time. I deleted every Personal Access Token and my pushes STILL authenticated, because Git Credential Manager uses an OAuth grant, which lives on a different settings page. Revoke it under Settings > Applications > Authorized OAuth Apps, then clear the cached credential. Revoking a GRANT invalidates every token ever issued under it, which is strictly stronger than deleting one token. Then verify: git ls-remote must FAIL and prompt for login. If it still succeeds, nothing was revoked.

Command

cmdkey /delete:LegacyGeneric:target=git:https://github.com
4

Step 4: Check deploy keys and SSH keys -- they survive every rotation

A write-enabled deploy key is per-repository and invisible from your account settings, so it outlives every password change, token revocation and SSH rotation. Check every repo via GET /repos/{owner}/{repo}/keys; I confirmed zero across all 75 of mine. Also review Settings > Keys and compare FINGERPRINTS, not titles -- the title is chosen by whoever added the key.

5

Step 5: Clean the remote by RESTORING from history, not reconstructing

The payload is appended, never substituted, so your original content still exists in git history. For each infected file, walk back to the newest commit whose blob lacks the payload and restore THAT blob. 48 of my 54 files were fixed this way -- byte-exact, zero guesswork -- and the remaining 6 came from clean local copies. Truncation is the last resort and is imperfect: the worm inserts a semicolon before its padding, so a truncated file will not match your original byte for byte.

Command

powershell -ExecutionPolicy Bypass -File clean_git_malware.ps1
Download script
6

Step 6: Forward-fix commits, not history rewrites

Commit the clean state ON TOP of the infected commit and push normally. No force-push, no rewritten SHAs, no broken clones for teammates, nothing lost. I applied 173 such commits across 75 repositories with zero aborts. The trade-off, stated honestly: the payload blob stays in old commits, inert unless someone checks out that exact commit. That beats force-pushing 173 branches and breaking every clone on the team.

7

Step 7: Delete the committed dropper -- this is what makes it recur

The step that ends the loop, and the one I missed on my first pass. Because .vscode/tasks.json and settings.json are committed, they reach every clone: someone clones, opens the folder, re-infects themselves, their token is stolen, and the attacker pushes again. Confirm the files are worm-dropped rather than yours by comparing blob SHAs across repositories -- identical blobs in unrelated projects means a dropped template. Delete them, unless git history shows a genuine earlier version worth restoring.

8

Step 8: Notify anyone your token could reach

Pull your GitHub Events API listing and read the repo field of every push. If a repository you do not own appears, your compromised credential wrote to someone elses project and only you can tell them. Two collaborator repos appeared in my burst; one had the payload sitting in postcss.config.mjs afterwards. Send the owner the repository name, the branch, the specific file and its byte size so they can confirm it in seconds rather than taking it on faith. Their CI and anyone cloning them is exposed until they know, because the payload executes at build time.

9

Step 9: Verify by reading the remote, never by trusting your own logs

Re-fetch every branch of every repository and re-check every indicator against what GitHub actually holds. Do not trust cleanup output -- mine reported ten successful pushes that had deleted nothing. Make sure the verifier covers every tracked file type, and enumerate repositories from the GitHub API rather than from your disk: 23 of mine had no local clone and would never have been found otherwise.

10

Step 10: Install the CI guard so it cannot return silently

Drop this workflow into .github/workflows/ in each repo, then make worm-guard a REQUIRED status check in branch protection (and block force-pushes). After that, any push or PR carrying the payload, the markers, C2 endpoints, a fake font, a folderOpen task or worm .gitignore entries fails the build automatically -- no manual scanning, and the merge is blocked. Pair it with a global pre-commit hook locally. The workflow runs on GitHub runners so it is OS-independent; the local hook is not, so adapt that to your platform.

Key takeaway

"Cleaning your local files alone is not enough, as the infected commits will still exist on GitHub. You must repair the remote history and rotate your credentials to be truly secure. You're doing great—almost there."

Prevention

Keeping your code safe tomorrow

With the malware removed and verified, the next step is a few standing habits that make reinfection far less likely for you and your team.

1

Layer 1 -- disable automatic tasks globally, forever

Set "task.allowAutomaticTasks": "off" in your GLOBAL user settings for VS Code and for Cursor separately. This is the kill switch: even if a dropper reaches your disk, it cannot execute itself. Note it is not sufficient on its own -- see Layer 3.

2

Layer 2 -- a global pre-commit hook, so it covers repos you have not created yet

Put the scanner in ~/.git-hooks and run: git config --global core.hooksPath ~/.git-hooks. That covers every repository on the machine, including future clones, instead of one repo at a time. Repos using husky or lefthook set their own core.hooksPath locally and keep working, because local config beats global. Scan STAGED content (git rev-parse :file), not the working file, or a clean file can mask dirty staged content.

3

Layer 3 -- a post-merge guard, because config payloads run at BUILD time

This is the gap almost everyone leaves open. Disabling autorun stops the .vscode dropper, but a payload sitting in postcss.config.js or vite.config.js executes whenever you run npm run dev or npm run build. So pulling infected code from a shared repository is still dangerous with autorun off. A post-merge / post-checkout hook that scans the worktree and WARNS (never blocks -- a failing pull is worse) tells you before you build.

4

Layer 4 -- a required CI check that scans every file type

A workflow on push and pull_request that fails on the padding-plus-bootstrap pattern, obfuscation markers, C2 endpoints, font files without valid magic bytes, folderOpen tasks and worm .gitignore entries. Scan ALL tracked files: my config-only scanner reported clean while route handlers were infected. Exclude your own security documentation by path, or the guard will flag your write-up about the worm.

5

Close both doors: force-push AND ordinary merges

My personal repos were reached by force-push; the shared client repos were reached by an ordinary pull-request merge that carried the payload into main. Branch protection on main -- require review, block force-push -- closes the first. A required status check closes the second. You need both, because they are different doors.

6

Treat tokens as the blast radius, not machines

The spread here needed no infected machine: a stolen token plus the GitHub API rewrites dozens of repositories in minutes. So when a teammate is compromised, the urgent action is that THEY rotate their credentials -- your own rotation does not protect a shared repository if their token is still live. Also check who has write access: I found exactly one outside collaborator across 75 repos, on a project I no longer needed.

7

Make a stolen token useless for the attack it actually performs

You cannot make a valid push credential unable to write, because writing is its purpose. But you CAN make the worm technique fail on the server no matter how valid the token is. The worm rewrites existing commits and force-pushes, so BLOCK FORCE-PUSHES on the default branch (a GitHub ruleset with a non_fast_forward rule). That one setting defeats the mass history-rewrite even with your real token, and it costs a solo developer nothing.

8

Require signed commits: every worm commit here was unsigned

A ruleset that requires signed commits on the default branch rejects unsigned commits on arrival, and the attacker cannot forge your signature because the signing key is separate from the push token and never leaves your machine. Set up SSH commit signing once (git config gpg.format ssh, then user.signingkey, then commit.gpgsign true), add that public key to GitHub as a SIGNING key (a separate entry from the auth key, the same key is fine for both), and only then enable the rule. Order matters: confirm your own signed pushes work first, or you lock yourself out.

9

Keep the daily credential unable to administer repos

The token Git Credential Manager uses for interactive pushes (a gho_ OAuth token) cannot change branch protection or rulesets: it returns 403 on those admin APIs. That is a feature, not a limitation. It means a stolen daily token cannot switch off its own guardrails. Leave it that way, and for automation use fine-grained tokens scoped to a single repo with an expiry, never a classic all-repo token, so one stolen automation token can reach one repo instead of forty-two.

10

Check whether YOUR token reached other people

This is the obligation people miss. Read the repo field of every push event in your GitHub Events API listing, not just your own namespace. Two of the repositories in my burst belonged to collaborators who had given me write access -- one of them had a 9,328 byte postcss.config.mjs afterwards. They had no idea. If your token could push somewhere, assume it did, and tell the owner with the specific file name so they can verify it themselves.

11

Audit shared repos for the dropper BLOB, not just for current files

The dropper is two fixed files, so their blob SHAs are stable fingerprints. Run git cat-file -e SHA in every shared repository: if the object exists, that dropper was committed there at some point and anyone who pulled it was exposed -- even if the files are long gone from the current tree. This is how I found my own entry point, in two team repositories whose history already contained cleanup commits from weeks before I knew anything was wrong.

12

Prove your scanner can fail

Keep one quarantined infected sample and run every new check against it. A check that reports clean because it is broken is indistinguishable from a clean result -- that is precisely how this survived two cleanups. I now test detection against real samples before trusting any scan.

If you only do one thing

Delete the committed .vscode dropper from every repository. Rotating credentials without doing that just means the worm steals your NEW token the next time anyone opens the folder -- which is exactly why this happened to me three times in two months. Cleaning configs treats the symptom; removing the dropper and revoking the OAuth grant breaks the loop.

Quick Answers

Frequently asked questions

A quick summary of everything we covered, perfect for sharing with teammates who might be in a rush or panicking.

What is the Git config injection worm?
It is a sneaky piece of malware that specifically targets developers by infecting Node.js and JavaScript project folders. Instead of attacking your whole computer, it waits for you to open an infected folder in VS Code. Once triggered, it quietly injects bad code into your config files, steals credentials, and rewrites your existing commits so the payload reaches GitHub without anything new appearing in your activity feed.
How can I tell if my system is infected with the Git worm?
Check three places, because most people only check the first. 1) Configs: open postcss.config.js, turn off word wrap and scroll fully right -- a wall of code after roughly 50 spaces is the payload. 2) Source files: it also lands in route handlers such as src/routes/auth.js, so scan EVERY tracked text file, not just *.config.js. 3) The remote: your machine can be completely clean while your GitHub repos are infected, so fetch every branch and scan what GitHub actually holds. Also check .vscode/tasks.json for a folderOpen task, and .gitignore for temp_auto_push.bat entries.
How do I safely remove the Git config injection worm?
Order matters more than tooling. 1) Disable automatic tasks in your GLOBAL editor settings. 2) Delete the committed .vscode dropper from every repository -- skip this and it simply returns. 3) Clean the payload on the remote by restoring each file from the newest commit in history that predates the infection, rather than reconstructing it by truncation. 4) Revoke the OAuth app grant for Git Credential Manager, not just your PATs, and confirm that git ls-remote now FAILS. 5) Verify by re-reading the remote, not by trusting your cleanup log. 6) Add a pre-commit hook, a post-merge warning and a CI check so it cannot return silently.
Why don't the malicious commits show up in my GitHub recent activity?
Not because they are backdated — that is a common misconception worth correcting, because it sends people hunting for old-dated commits that do not exist. The worm rewrites the commit already at the tip of your branch and preserves its original author date exactly. Since GitHub sorts and displays history by author date, the rewritten commit stays exactly where the original was and looks unchanged. The giveaway is that the commit SHA changed while the message and date did not — which most people notice first as an unexplained Vercel or CI deployment, not as a strange entry in their feed.
How can I protect my projects from getting infected again?
The best defense is simple: go into your global VS Code user settings and set `task.allowAutomaticTasks` to `off`. Additionally, get into the habit of taking a quick peek at the bottom of your root config files before diving into code, especially if you work on a shared team drive where a colleague's computer might be infected.
Is this the same worm discussed in GitHub Community discussion #188732?
Yes. The behavior described here — obfuscated payloads appended to `postcss.config.js`, `next.config.js`, and similar files using the `global['!']` signature, along with fake font files and silently rewritten Git commits — exactly matches what multiple developers reported in GitHub Community discussion #188732. This is the same self-propagating Git worm, and you are absolutely not alone in encountering it.
Will Malwarebytes or Windows Defender detect the Git config injection worm?
In most cases, no — and this is what makes it so dangerous. Unlike typical malware, this worm operates entirely within legitimate developer tools: Node.js, VS Code tasks, and standard Git commands. Tools like Malwarebytes and Windows Defender are optimized to detect executable payloads, not obfuscated JavaScript hidden inside config files. Microsoft Defender SmartScreen and CrowdStrike Falcon may flag network calls to the known C2 IP (166.88.54.158), but the local file infection itself typically goes undetected by standard antivirus. Your best scanner is the manual audit described in this guide.
What is the fa-solid-400.woff2 file and why is it dangerous?
Despite having a `.woff2` font extension, `fa-solid-400.woff2` and its variant `fa-regular-400.woff2` are not web fonts at all. They are obfuscated Node.js scripts acting as the primary payload dropper. The worm disguises them as common FontAwesome files because these names appear harmless in web projects. When VS Code runs the automatic task, it executes this fake font file using Node.js, triggering the full infection chain across your project folders.
Can this worm steal my API keys or .env file secrets?
Yes, and this risk must be assumed. Once active, the worm can read environment variables and local files in the infected directory. Any `.env` files, API keys, GitHub Personal Access Tokens (PATs), or SSH private keys in or near an infected project should be treated as compromised. Regardless of whether you can confirm data exfiltration, rotate all credentials from any infected folder. This is non-negotiable.
How do I clean my GitHub remote history after a Git worm infection?
Prefer FORWARD-FIX commits over history rewrites. For each infected file, find the newest commit whose blob predates the infection and restore that exact blob, then commit the clean state on top and push normally -- no force-push, no changed SHAs, no broken clones for your team. I applied 173 such commits across 75 repositories with zero failures, and 48 of 54 files were restored byte-exact from history. The honest trade-off is that the payload blob remains in old commits, inert unless someone checks out that specific commit. `git filter-repo` does erase it completely, but it rewrites every SHA and forces everyone to re-clone, so reach for it only if you truly need the history purged.
Why did the worm keep coming back after I cleaned it?
Almost certainly because the dropper is still committed in your repositories. `.vscode/tasks.json` and `.vscode/settings.json` are checked in, so they travel to every clone: someone opens the folder, the folderOpen task runs, a token is stolen, and the attacker pushes again. Cleaning config files treats the symptom. It recurred three times for me because two cleanups removed the payload but left the dropper and left the OAuth grant live.
My antivirus and my own scanner both say my machine is clean. Can I still be infected?
Your machine can be genuinely clean while your GitHub repositories are thoroughly infected -- that was exactly my situation at every check. The spread was server-side through the GitHub API using a stolen token, so no local infection was required. Verify by reading the REMOTE: fetch every branch of every repo and scan what GitHub actually holds. Enumerate repos from the GitHub API too, not from your disk -- 23 of my infected repos had no local copy at all.
Is rotating my GitHub token enough to stop it?
No, and doing it in the wrong order wastes the effort. If the committed dropper is still in your repos, the worm simply steals your NEW token the next time anyone opens an affected folder. Remove the dropper first, then rotate. Also revoke the OAuth app grant for Git Credential Manager, not only your Personal Access Tokens -- I deleted every PAT and my pushes still authenticated, because the live credential was an OAuth grant on a different settings page.
Will disabling automatic tasks fully protect me?
It stops the dropper from auto-executing, which is essential, but it is not complete. A payload appended to `postcss.config.js` or `vite.config.js` runs whenever you execute `npm run dev` or `npm run build`. So pulling infected code from a shared repository is still dangerous with autorun disabled. Add a post-merge hook that scans the worktree and warns you before you build.
Does this affect Python or other non-JavaScript projects?
Yes, but not through your Python code. The worm is language-agnostic in what it DROPS: it plants the same three things in any repository regardless of language, a .vscode/tasks.json auto-run task, a fake fa-solid-400.woff2 that is really JavaScript, and worm entries in .gitignore. Several of my Python scraper repos were hit this way. What it does NOT do is inject into .py files or use Python auto-run vectors (sitecustomize.py, .pth files, conftest.py) -- I verified all of those were clean. The reason is that its execution vector is Node: the VS Code task runs the fake font with `node`, so a project only actually executes the payload if Node runs in it. Treat the dropped files as inert-but-present in a Python repo: still remove them, because anyone who opens the folder in VS Code with auto-tasks enabled will trigger the Node step.
My staging and production suddenly show the same thing / a branch history disappeared. Did the worm delete my work?
This is the force-push doing its damage. The worm force-pushes, which replaces a branch instead of adding to it, and in at least one of my repos it collapsed staging onto the main/landing history -- so ~2 months of feature commits vanished from the remote and staging started serving the same site as production. Two things to know: (1) the loss is on the REMOTE only -- a force-push cannot touch your local clone, so if you have the repo locally your real history is intact; (2) recover it by backing up first, confirming the local tip is clean, then git push --force-with-lease origin <branch> to restore the remote from local -- which also redeploys the correct app. Spot it with git status showing your branch is "ahead" of origin by many commits, or two branches whose remotes point at the same commit. If you have no local copy, contact GitHub Support fast -- they can sometimes recover a branch from its server-side reflog before garbage collection.