Polin Rider Attack
Jul 2, 2026 · 13 min read
This is the story of how a stolen GitHub token was used to force-push malware into multiple repositories — including several of my own — how we traced it, and what it taught me about trust, Git, and the backups you don’t know you have. It began with a notification nobody thought twice about.
The push nobody questioned
June 4, 2026 · Around 2 AM
It started with a GitHub notification.
Several developers received emails saying that a trusted account had force-pushed repositories they had been actively working on. GitHub was simply informing them that the repositories’ history had been rewritten.
Nobody questioned it.
Not because force-pushing was something that happened regularly, but because the notification named an account we trusted. We assumed there was a good reason for it. Maybe it was part of a migration, a cleanup, or some maintenance work we hadn’t heard about yet. Whatever the reason, none of us thought much of it.
So we went to sleep.
The next morning, everything felt normal — until another developer started working on one of the repositories. They noticed that files they knew should have been there were missing.
At first, it didn’t immediately point to an attack. It could have been a bad merge, an accidental force push, or some other mistake. But the force-push notifications from the previous night suddenly came back to mind.
Instead of making assumptions, they contacted the owner of the account to ask what had happened.
The reply changed everything.
The account owner hadn’t pushed anything.
That was the moment we stopped treating it as a Git mistake and started treating it as a security incident. We didn’t know it yet, but we were about to spend the next several weeks tracing stolen credentials, reverse-engineering malware, recovering repositories, and learning far more about Git than any of us had ever expected.
Following the first clue
The moment the account owner replied that they hadn’t pushed anything, the situation changed. What had looked like an unusual Git operation was now something much more serious. Someone had rewritten multiple repositories, and GitHub believed those actions had come from a trusted account.
As we reviewed the activity from the previous night, another detail stood out. One of my pull requests had been merged by another developer. At first, there was nothing unusual about that — they regularly reviewed and merged my pull requests.
But while looking more closely at the merge, they noticed something that didn’t make sense. They always sign their commits and pay close attention to GitHub’s verification details. This merge didn’t look like one that had originated from them. The more we investigated, the stranger it became: although it appeared to be related to their merge, the underlying identity associated with the action belonged to the trusted account.
That was the moment we realized this wasn’t a Git mistake. Someone was performing actions using a valid credential from that account.
A support ticket was immediately opened with GitHub, requesting the complete activity history and any information that could help identify how these actions had been performed. While waiting for GitHub’s response, we resisted the urge to start making changes. Instead, we focused on understanding what had happened.
Every force push. Every rewritten branch. Every unexpected merge. Every timestamp. We wanted to understand the attack before attempting any recovery.
When GitHub eventually responded, they confirmed something that changed the direction of the investigation. The attacker hadn’t bypassed GitHub’s security. They hadn’t exploited a vulnerability. Every suspicious action had been performed using a valid Personal Access Token (PAT). From GitHub’s perspective, every request was legitimate because it had been authenticated with a valid credential.
For the first time, we knew how the attacker had interacted with GitHub. But one question remained: how had the PAT been stolen?
Following the PAT
Now that we knew the attacker had used a valid Personal Access Token, the investigation shifted in a completely different direction. The question was no longer who had force-pushed the repositories. It was where that token had come from.
Investigation traced the stolen credential to a shared build environment that had previously executed code from a compromised repository. When the malicious code ran, it gained access to credentials available in that environment.
From there, the attack spread. With the stolen token, the attacker was able to rewrite multiple repositories while appearing completely legitimate to GitHub.
We finally knew where the credential had come from. The next challenge was understanding how the malware had remained hidden for so long.
Hiding in plain sight
Finding the compromised build environment answered one question. It immediately raised another: how had the malware managed to stay hidden for months?
At first, we expected to find an unfamiliar executable, a suspicious process, or some obvious piece of malware sitting somewhere on the machine. We found none of those. Instead, the malicious code was hiding inside files we looked at almost every day. Configuration files. Build scripts. Files that most developers rarely read once they’ve been created.
The first samples we found had been appended to files such as:
tailwind.config.jspostcss.config.mjs
The payload wasn’t placed at the top of the file. It wasn’t even visible unless you scrolled all the way to the end. Thousands of whitespace characters separated the legitimate configuration from a heavily obfuscated block of JavaScript.
On a normal code review, nothing looked unusual. The configuration still worked. The application still built successfully. Most developers would never notice anything had changed.
The more samples we analyzed, the more we realized this wasn’t limited to build
configuration files. One repository contained something even more deceptive. Its
.gitignore had been modified to include the .vscode directory. Inside it was
a tasks.json file configured to execute automatically whenever someone opened
the project in Visual Studio Code.
The task appeared to execute a font file:
public/fonts/fa-solid-400.woff2
Except it wasn’t a font. It was JavaScript. Opening the repository in VS Code was enough to execute it.
The malware wasn’t relying on sophisticated exploits. It was relying on something much simpler: it abused tools developers trusted every day. Configuration files. Build scripts. Editor automation. Files we almost never question.
Once we understood that pattern, we knew what to look for. And suddenly we started finding the same fingerprints everywhere.
Assuming every machine was compromised
By this point, we understood how the attacker had gained access. What we didn’t understand was how far that access had spread. We had no way of knowing which machines were safe and which ones had already been compromised.
So we made one assumption. Every machine was potentially infected.
I didn’t want to take any chances. The first thing I did was revoke every SSH key associated with my GitHub account. Then I wiped my laptop completely and started over with a fresh installation. Once everything was set up again, I generated a fresh set of SSH keys and continued working.
For a while, I stopped using Visual Studio Code altogether. The attack had hidden itself inside files developers interacted with every day, and at the time I wasn’t comfortable trusting editor extensions or automatic tasks until we understood exactly what had happened. I switched to Zed and continued the investigation from there.
While all of this was happening, we faced another challenge. If every potentially affected developer was about to wipe their machine, how could we make sure we didn’t lose the clean copies of repositories that still existed on those laptops?
Ironically, the safest copies of many repositories weren’t on GitHub anymore. They were sitting inside developers’ local clones. We needed to preserve them before anyone formatted their machine.
To make that process easier, I built a small tool that allowed every developer to register the folders containing their local repositories with a single command. Instead of manually copying projects one by one, everyone could quickly archive their repositories into a common backup location before wiping their devices.
The idea was simple. Preserve everything first. Clean the machines second. Those local clones would eventually become the foundation of our recovery effort.
At that point, I still believed the attack was limited to the shared environment. I knew I still had a Personal Access Token on my own GitHub account, but I wasn’t particularly concerned about it. I was confident I had never used it in any of the affected repositories, and I assumed it was only being used for one of my personal projects. I told myself I’d investigate it once the initial incident was contained.
I never got the chance.
The 3:30 AM phone call
I thought the worst was behind us. We had identified the attack vector. We were preserving local repositories, and recovery was underway. I was focused on helping with that effort and assumed my own GitHub account wasn’t part of the incident.
Then, a few days later, at around 3:30 in the morning, my phone rang. It was another developer involved in the investigation. Still half asleep, I answered the call.
The first thing they told me was something I wasn’t prepared to hear. The latest attack had been traced back to my GitHub account.
For a few seconds, I didn’t know what to say. Up until that moment, I had been helping recover other affected repositories. Now I was part of the incident.
The first thing they asked me was whether I still had any active Personal Access Tokens. I did. I had never used that token in any of the affected repositories, so I hadn’t considered it an immediate risk.
I was wrong. GitHub had already confirmed that the attacker was abusing Personal Access Tokens. The moment I realized my token could also have been compromised, there was no time to investigate. We immediately revoked it.
Within minutes, I warned everyone who might be affected that my account had been compromised. The attack reached several repositories, including some of my own personal projects.
Following guidance from another developer, we treated my machine the same way we had treated every other potentially compromised device. I generated another fresh set of SSH keys. This time, instead of storing them directly on my machine, I configured 1Password’s SSH agent to manage them. We also reviewed my GitHub account, enabled the appropriate security protections, and made sure every remaining credential had been rotated.
Only after every credential had been replaced did the attacks originating from my account stop.
At that point, I finally understood something I hadn’t fully appreciated until then. The incident wasn’t over. It had simply reached me later than everyone else. And now I had another problem to solve: how do you recover repositories that you own after an attacker has force-pushed nearly all of them?
The backup I didn’t know I had
By the time my own repositories were affected, I had already watched other repositories being recovered. I knew one thing: force-pushing a repository doesn’t immediately destroy its history. The real challenge was figuring out whether a clean copy still existed somewhere.
For me, that “somewhere” turned out to be my own laptop. Although my GitHub repositories had been rewritten, I had never pulled those changes into my local clones. Without realizing it, those local repositories had become snapshots of the projects before the attack. For the first time since this all began, I felt like recovery might actually be possible.
I didn’t immediately start force-pushing branches back to GitHub. The first question I needed to answer was much simpler: were my local repositories actually clean?
That meant checking every branch. Inspecting commit history. Searching for indicators of compromise. Looking through dangling commits. Comparing local references with GitHub. Understanding what Git knew that I didn’t.
One by one, I verified every repository. Some were exactly as I had left them. Others required much deeper investigation. One repository had even lost an entire branch on GitHub. Another had only its latest commit compromised. Every repository became its own puzzle.
What surprised me most wasn’t the malware. It was Git. Before this incident, I had used Git every day without thinking much about how it actually stored history. Over the following days I found myself learning about reflogs, unreachable objects, remote-tracking references, pull request references, garbage collection, and the countless ways Git tries to preserve history — even after you think it’s gone.
Some recoveries were straightforward. Others required reconstructing commits manually before pushing them back. Eventually, every one of my personal repositories was recovered. Some through clean local clones. Some through careful reconstruction. None of them because I had a proper backup strategy.
The backup that saved my projects wasn’t a cloud snapshot. It wasn’t an external drive. It wasn’t an automated backup service. It was a local Git clone that I’d simply never pulled.
That realization completely changed the way I think about Git. The exact recovery process deserves its own article — because recovering those repositories taught me more about Git internals than years of using Git ever had.
What this incident taught me
When this all began, I thought I was dealing with a GitHub incident. In reality, I was learning lessons about trust.
We trusted a GitHub notification because it named a familiar account. We trusted configuration files because they looked like configuration files. We trusted build environments because they were part of a familiar workflow. We trusted that repositories on GitHub represented the source of truth.
Over the following weeks, every one of those assumptions was challenged.
The incident forced me to learn far more than I ever expected. Before this happened, Git was simply a version control system I used every day. Afterwards, I found myself learning about reflogs, unreachable objects, remote-tracking references, pull request references, garbage collection, and the surprising number of ways Git tries to preserve history.
I also learned that a repository isn’t necessarily lost just because its history has been force-pushed. Sometimes the most valuable copy of your project isn’t the one hosted in the cloud. Sometimes it’s the local clone sitting on a laptop that hasn’t pulled in weeks. That single realization changed how I think about backups forever.
The incident also changed the way I approach security. Today, I rotate credentials more aggressively. I pay much closer attention to where secrets are stored. I treat build configuration files as executable code instead of harmless configuration. And I no longer assume that tools I use every day are inherently trustworthy.
Looking back, I’m grateful that the damage was recoverable. Not every supply chain attack ends that way. We recovered the repositories, cleaned the affected environments, and rotated credentials. Most importantly, we shared what we learned with each other.
If there’s one thing I hope you take away from this story, it’s this: security incidents rarely begin with something that looks obviously malicious. Sometimes they begin with an email that seems completely ordinary. Or a configuration file that nobody thinks to inspect. Or a local Git clone that quietly becomes the only remaining copy of a project.
The best backup I had wasn’t a cloud snapshot. It wasn’t an external drive. It wasn’t an automated backup service. It was a local Git clone that I’d never pulled.
I never expected a supply chain attack to teach me so much about Git. But in the end, that’s exactly what it did.
I wrote up the exact recovery process — the reflogs, unreachable objects, pull-request refs, and the commands I used to rebuild poisoned repositories — as a separate technical follow-up: Recovering from the Polin Rider Attack.