The short version
My GitHub personal access token was exfiltrated — most likely by a supply-chain compromise somewhere in my developer environment. The attacker used it to push malicious commits to several of my private repositories. One of those repositories belonged to a client. The client ended the engagement. They were right to.
That's the whole of what happened. Everything I want to say in this post is about what I did afterwards, and about what I am now building and studying because of it.
The attack, by category
I can't tell you exactly which package, extension, or tool in my local environment exfiltrated the token. By the time I was thinking clearly enough to investigate, a lot of the evidence on my machine had been rotated out by the ordinary churn of a working developer's day — extension updates, shell-history truncation, containers torn down and rebuilt.
What I can tell you is the shape of it, because the shape is the part that matters and the shape is depressingly common in 2026. A developer installs a dependency, or updates an editor extension, or pulls a Docker image. Something in that chain runs code with the developer's privileges. That code reads the developer's environment — shell vars, ~/.netrc, the file a coworker swore was safe to leave in ~/.config, the GitHub CLI token cache — and ships it to an attacker.
The token is the goal. The token is everything. With a developer's GitHub PAT, an attacker can do a precise, quiet version of the loudest possible attack: push commits as you, tag releases as you, approve deployments as you, and do all of it before the timezone you live in wakes up and notices.
This is not a new class of attack. It has been visible for years, and the public incidents — the xz-utils backdoor, the eslint-scope takeover, the nx compromise, npm and PyPI events I don't need to name — have been teaching the industry the same lesson repeatedly. I had read about every one of them. I read security books for fun. I work at a cloud-security company. I thought I had the basics covered.
I didn't.
The lag
What surprised me wasn't the attack. It was the gap between being hacked and starting to do anything useful about it.
For a few days, I mostly did the administrative things. Revoked the token. Rotated everything the token could have touched. Told the people I needed to tell, in the order that was least painful for them. Wrote down what I knew. None of that was engineering. All of it felt like the wrong muscle to be using.
The engineer in me came back online later. The first thing it did surprised me: it didn't go to my client's stack or to the frontpage of Hacker News. It went to my own code.
The conversation I don't want to write about but should
The client conversation was the hardest part and I'm going to keep it short because I've promised myself not to make this post about anyone but me.
When malicious commits show up in a client's repository, signed under your identity, the client doesn't owe you the benefit of the doubt. Nor should they. The question they had to answer wasn't "what happened to Princewill," it was "can we continue with someone whose keys were demonstrably in someone else's hands this week." The reasonable answer to that question, on their timeline and with their information, was no.
I would not have given me the benefit of the doubt either. I hope the next client who has that conversation about someone else is as firm as this one was.
That sentence took me a long time to arrive at. It is not the one I wanted to write the week after it happened.
What I did with the logs
Here is the part that is enjoyable to write about.
When something has gone wrong, engineers pull logs. So that's what I did — not as professional incident response, just as a senior engineer trying to understand their own machine. Rough order of what I looked at:
GitHub's audit log for my account. Every auth event, every push, every token use, scoped by IP, user-agent, and time. Extremely valuable. This is the single most important artifact in an incident like mine and the one I'd recommend every engineer pin in a browser tab forever.
Shell history across my shells, looking for commands I didn't recognize running in directories I didn't expect.
Cron and systemd-timer units. A favorite hiding place for persistence that isn't exotic but is easy to forget.
Editor extension install and update history. The tools we trust the most are the ones we install the most silently.
Container and V.M snapshots I still had. Useful for one of my repositories, not for the rest.
D.N.S query logs from my home router. Noisy, but occasionally a goldmine if you know what to look for.
Network-interface counters and ip-tables traces. Mostly just confirmed I'd been too relaxed about what processes could talk outbound.
I built a timeline. Most of it was fruitful only in retrospect; very little of it would have been fruitful if I'd started earlier, because I didn't have the telemetry I would have wanted retained. That was a lesson unto itself: you cannot forensically reconstruct what you did not choose to record.
I did not figure out, with certainty, the precise dependency or extension that took the token. I have a short list of suspects. I have no intention of publishing the list, because a published accusation without a confirmed provenance is irresponsible, and because it wouldn't help anyone — the right answer is the systemic one, and the systemic answer applies regardless of which package did it.
What I recognized in my own practice
Here is what I was embarrassed by when I looked at my own habits, with the specific attack in mind:
My tokens were too powerful. I had a small number of personal access tokens with broad scope and long lifetimes. The attacker didn't need to pick a token carefully; any of mine would have worked beautifully. Fine-grained PATs, short TTLs, and per-purpose tokens were features I had known about for years and not adopted for my own account.
I didn't sign commits. Commit signing with branch-protection rules on main would have stopped the attack on the server side, at zero cost to me as a developer, because the attacker with a stolen PAT still couldn't sign as me.
My supply-chain hygiene was checklist, not practice. I ran go mod verify and npm audit occasionally. I did not have a repeatable workflow that asked, for every new dependency or editor extension, "what privileges does this acquire on my machine, and is it worth it?"
I had no meaningful alerting on my own GitHub account. Unusual push patterns, new deploy keys, new webhook URLs, new collaborators — all the things an attacker does immediately after acquiring access — could have paged me within minutes of happening. None of it did.
I had no incident-response playbook for myself. I had written runbooks for production services. I had written nothing for my own identity. The first few hours I lost to administrative paralysis were the hours a written playbook would have saved.
None of that list is exotic. All of it is covered in the first chapter of every defensive-security book I own. The gap between knowing about something and having done it was, as it usually is, where the bad day happened.
What I am doing about it
A few concrete things, because the most annoying version of this kind of post is the one that ends with "and so I resolved to do better."
- I started a structured study track.
Graduate-level material, one module a week, notes I actually use: Schneier's Applied Cryptography, Ferguson & Kohno's Cryptography Engineering, Aumasson's Serious Cryptography, the O'Reilly Defensive Security Handbook, Du's Computer & Internet Security. I'm going to audit the cloud infra of a few companies later this year for free, not for the charity, but for the forcing-function of hands-on work I would otherwise skip.
- I'm building a secure messenger in Go.
It's called whisperd. Go services, gRPC internals, NATS JetStream fabric, end-to-end encryption via the Double Ratchet, mTLS everywhere, Linkerd mesh in a Kubernetes cluster, and a pile of deliberately written Architecture Decision Records in docs/adr/ that explain, in short-enough-to-read terms, why each security choice is the way it is.
It's not a product. It's a laboratory — a place to practice the things I'd been getting away with not practicing. Every ADR is a piece of knowledge I've committed to defending in public. The most uncomfortable one to write was the threat model: I had to sit down, for a toy project, and systematically ask "who would attack this and how." It's an exercise I should have been running on every serious codebase I've touched since 2016.
- I rebuilt the way I carry keys around.
Fine-grained PATs, short lifetimes, per-purpose tokens, committed signing, branch-protection rules wherever I have branches worth protecting, a hardware key for second factor, a password manager I actually generate entropy into, separate developer machines for separate trust levels, least-privilege everything. None of it is exotic. The difference is I'm doing it.
- I wrote the incident playbook I wish I'd had.
Three pages. First hour: revoke, rotate, record. First day: log-pulling checklist, disclosure list, communication templates. First week: public write-up, learnings document, calendar reminder at one month to review. It lives in the same repository as my shell dotfiles, because that's where I'll actually find it at 2 a.m.
Five habits for backend engineers
If I could send one email to everyone I know who writes backend code, with the specific shape of this incident in mind:
- Scope your PATs aggressively. Better: don't use them.
Fine-grained tokens. Short lifetimes. Per-purpose. Never one token that can push to everything you own. Prefer SSH keys with commit-signing enabled, GitHub Apps for automation, and OIDC federation for CI over long-lived tokens. A personal access token with repo scope and no expiry is, in 2026, an accident waiting to happen.
- Turn on commit signing and branch protection on main today.
Required signed commits + branch protection is the single highest-leverage defence against the exact incident I had. Even with a fully valid PAT, the attacker cannot sign as you. GitHub supports both. It costs you ten minutes to configure.
- Audit your supply chain like you audit your prod.
govulncheck + gosec + pinned versions with checksum verification for Go; pip-audit + pip install --require-hashes for Python; npm audit + lockfile commits + npm ci for Node. Turn them into CI jobs that fail loud. Do the same pass for your editor extensions and container images. Treat your developer machine as prod. It's the weakest prod you have.
- Alert on your own identity.
GitHub will email you when a new deploy key, SSH key, webhook, or unusual sign-in appears. Those emails should go to a filtered inbox you actually read. Better: a pager. When someone else has your keys, you want to know in minutes, not in days.
- Write your personal incident playbook before you need it.
One page is enough. What do you revoke first? Who do you tell, in what order? What logs do you pull, in what order? What communications templates do you send? You will not be thinking clearly when you need it. Your past self can.
One thing I keep thinking about
I work at a cloud-security company. I read the books. I had read, in full, the post-mortems of every supply-chain incident I could get my hands on. And a token of mine still left my machine.
The lesson I keep turning over is that the gap between knowing security and practicing security is enormous, and for most of us senior backend engineers, it is the gap where the bad days happen. Closing it isn't exotic. It's mostly about turning things you already know into reflexes you don't have to think about.
I am a better engineer than I was six weeks ago. Not because I learned something new, but because I finally started doing the things I already knew.
Where to find me
Code: github.com/just-nibble
Email: princewillolaiya@gmail.com
If you're building secure backend systems in Go, I'd like to know what you're working on. If you've had the same kind of bad day and want to compare notes, my inbox is open. If you find something I missed, please reach out — I would genuinely rather hear it from you than from whoever rotates through my repos next.
Top comments (0)