Polin Rider Attack
Jul 2, 2026 · 14 min read
This is the story of how a stolen GitHub token was used to force-push malware into repositories across three organizations — 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.
Our team received emails saying that our CTO had force-pushed several repositories. The repositories were ones my team had been actively working on, and GitHub was simply informing us that their history had been rewritten.
Nobody questioned it.
Not because force-pushing was something that happened regularly, but because the notification came from our CTO. We assumed there was a good reason for it. Maybe it was part of a migration, a cleanup, or some maintenance work that we hadn’t been told about yet. Whatever the reason, none of us thought much of it.
So we went to sleep.
The next morning, everything felt normal — until my senior developer started working on one of the repositories. He noticed that files he 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, he reached out to our CTO to ask what had happened.
The reply changed everything.
He was traveling. And he 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 our CTO replied that he 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 our CTO’s account.
As we reviewed the activity from the previous night, another detail stood out. One of my pull requests had been merged by my senior developer. At first, there was nothing unusual about that — he regularly reviewed and merged my pull requests.
But while looking more closely at the merge, he noticed something that didn’t make sense. He always signs his commits and pays close attention to GitHub’s verification details. This merge didn’t look like one that had originated from him. The more we investigated, the stranger it became: although it appeared to be related to his merge, the underlying identity associated with the action belonged to our CTO.
That was the moment we realized this wasn’t a Git mistake. Someone was performing actions using our CTO’s credentials.
Our CTO immediately opened a support ticket 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 our CTO’s 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 our CTO’s PAT been stolen?
Following the PAT
Now that we knew the attacker had used our CTO’s Personal Access Token, the investigation shifted in a completely different direction. The question was no longer who had force-pushed our repositories. It was where that token had come from.
At our company, every Shopify app eventually passed through a single Mac Studio before being released. It wasn’t a developer machine. It was our build server. Every new build, every release candidate, every test build eventually ended up there.
Since the attacker had authenticated using our CTO’s PAT, the obvious place to begin was the machine that actually stored that credential. So we started digging.
As we investigated the build server, we discovered something unexpected. The compromise hadn’t started there. Tracing it backwards led us to one of our client’s repositories that had been infected months earlier, sometime around February or March. At some point, that repository had been built on the Mac Studio.
The malware executed. The credentials stored on the machine became accessible. From there, the attack spread. The CTO’s Personal Access Token was stolen. And with that single credential, the attacker was able to rewrite repositories across multiple organizations 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 server 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, the company was facing another challenge. If every developer was about to wipe their machine, how could we make sure we didn’t lose the clean copies of our 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 had originated entirely from our company’s infrastructure. 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 our company repositories, and I assumed it was only being used for one of my personal projects. I told myself I’d investigate it once the company was stable again.
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. The company had begun recovering its projects. 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 my senior developer. Still half asleep, I answered the call.
The first thing he 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 repositories that belonged to the company. Now I was part of the incident.
The first thing he asked me was whether I still had any active Personal Access Tokens. I did. I had never used that token in any of our company 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 across every GitHub organization I had access to that my account had been compromised. In total, the attack affected around nine or ten repositories spread across three different organizations, including several of my own personal projects.
Following my senior developer’s guidance, 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 the company recover dozens of projects. 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 came from our CTO. We trusted configuration files because they looked like configuration files. We trusted build servers because they had always been part of our deployment pipeline. We trusted that our 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 our repositories. We rebuilt our infrastructure. We rotated credentials. We learned. 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.