The OpenAI npm Attack Was a Git Mistake. You Probably Made It Too.
Six minutes. 84 poisoned packages. Two compromised devices at OpenAI. The trust boundary that broke is the same one most senior engineers…
The OpenAI npm Attack Was a Git Mistake. You Probably Made It Too.
Six minutes. 84 poisoned packages. Two compromised devices at OpenAI. The trust boundary that broke is the same one most senior engineers cross every week without thinking about it.

On May 11, 2026, between 19:20 and 19:26 UTC, an attacker published 84 malicious package versions across 42 TanStack libraries. The window was six minutes. The detection happened in twenty. By the time TanStack pulled the versions, two devices inside OpenAI had already run npm install.
OpenAI confirmed the breach four days later. Two employee devices. Limited credential exfiltration from internal source code repositories. The malware that ran on those laptops did not come from a hacker stealing OpenAI’s tokens. It came from TanStack’s own legitimate release pipeline, using TanStack’s own trusted OIDC identity, publishing code that had been silently rewritten by an attacker who never needed credentials at all.
The attacker’s entire toolkit was three things. A forked GitHub repository. A pull request. And a cache that everyone in the workflow trusted because the workflow had always trusted it.
This piece is for the senior engineer reading the headlines and assuming this was an npm problem. It was not. It was a Git workflow trust problem, dressed up in CI/CD clothes. The same trust assumption that broke TanStack’s pipeline is the assumption you make every time you write a Git workflow that touches secrets. You have probably written one this quarter.
If you are wondering whether you would have caught this attack before it hit, run the audit at the bottom of this piece. Most senior engineers I have shown it to find at least two of the five patterns in their own workflows.
What actually broke
The headline version of the attack is that npm got poisoned. The headline version is wrong. npm did exactly what npm was designed to do. It published packages from a trusted publisher, signed with a valid OIDC token, on a release workflow that completed successfully.
The real failure happened upstream. TanStack’s CI workflow used a GitHub Actions feature called pull_request_target. Most senior engineers have either used it or copied it from a Stack Overflow answer without reading the warning at the top of the GitHub docs. The warning says: do not use this for workflows that do dangerous processing of fork code.
pull_request_target runs in the context of the base repository, not the fork. It has access to base repository secrets. It has access to base repository write permissions. And when it runs on a pull request from a fork, the code that triggers it can write to caches that the base repository's other workflows will later read and trust.
That is the trust boundary. A workflow that runs fork code inside the base repository’s permission context, writing to a cache that the release workflow will later restore, no questions asked.
The attacker exploited it. They forked TanStack’s router repository, renamed the fork to hide it from contributor searches, and opened a pull request. That PR triggered the pull_request_target workflow. The workflow checked out the attacker's code and ran it. The code wrote a malicious pnpm store into the GitHub Actions cache. When a legitimate maintainer PR was merged to main two days later, the release workflow restored that poisoned cache. The release workflow then published 84 versions with attacker-controlled code, signed with TanStack's real OIDC token, distributed to every developer who ran npm install in the next twenty minutes.
OpenAI’s two devices were downstream of that resolution. They installed a TanStack package version that had been signed by TanStack’s legitimate pipeline. From the security tooling’s perspective, the install was clean. Provenance was valid. Signatures were correct. The package was poisoned anyway.
Why this is a Git mistake
The CI vulnerability is real. GitHub will probably patch the cache trust boundary in the next quarter. The underlying mistake, the thing that made the attack possible, is the same mistake senior engineers make in their Git workflows constantly.
The mistake is trusting a state without checking who put it there.
pull_request_target trusts the cache without checking who wrote to it. A senior engineer running git reset --hard origin/main trusts the remote state without checking what happened to their local state. A senior engineer running git stash pop trusts that the stash contains what they think it contains. A senior engineer force-pushing a rebased branch trusts that they did not drop a commit. A senior engineer running git add . trusts that the working directory contains nothing they did not mean to commit.
Every one of these is a trust assumption you cannot see until it fails. The TanStack attack was the same shape. Cache contents trusted, never verified, exploited in a six-minute window.
The five patterns below are the Git versions of the same mistake. Each one is a trust boundary you have probably crossed this quarter. Each one has a recovery path you may not have used in months.
Five Git trust failures most senior engineers have
The pattern names are mine. The mistakes are universal.
Reflog Amnesia. The reflog is a journal of every move HEAD has made in your local repository. Default expiration is ninety days for reachable entries, thirty days for unreachable ones. It is your three-month window to recover almost any Git mistake that did not involve uncommitted work.
git reflog
The output shows every commit, checkout, reset, rebase, and merge with the SHA before and after. If you reset your local branch to remote and lost six hours of work, that work is still in the reflog. git reset --hard <sha> from the reflog brings it back.
Reflog Amnesia is not knowing this exists. The reason senior engineers have it worse than juniors is that juniors at least Google their Git mistakes. Seniors assume they know what to do, panic when their assumption is wrong, and spend hours rewriting work that was sitting in the reflog the whole time.
Run git reflog on your active repo. If you cannot remember the last time you typed that command, you have it.
The Stash Graveyard. git stash is the emergency pause button. You stash your work, switch branches, fix the bug, come back, git stash pop. That is how it is supposed to work.
How most senior engineers actually use it is that they stash work, switch contexts, get pulled into something else, never come back, stash more work two weeks later for a different reason, and gradually accumulate unlabeled stashes.
git stash list
Run it. If you have more than three entries you cannot identify, you have a Stash Graveyard. The problem is not disk space. The problem is that you no longer know what is in any of those stashes, and the longer you wait the less recoverable they become.
Two fixes. Name your stashes with git stash push -m "auth refactor, 70% done". Audit quarterly with git stash show -p stash@{n} and turn the useful ones into branches.
Force-Push Forgiveness. I knew force-push was dangerous on shared branches. I knew not to do it on main. What I did not know was that I had been routinely force-pushing my own feature branches in ways that overwrote work I had not yet pushed elsewhere.
Pattern looked like this. Rebase feature branch on top of latest main. Rebase feels clean. Force-push to update remote. Three days later realize a commit got dropped during the rebase that did not show up in the diff because the change had been small. Commit gone from local. Gone from remote. Work was real. Commit was not in the filesystem anywhere.
Except it was. The reflog had it for ninety days. The commit object was still in .git/objects/ until garbage collection ran. git fsck --lost-found would have shown it as a dangling commit, recoverable with one git branch recover <sha> command.
The fix is two habits. Before any rebase that touches more than two commits, create a backup branch with git branch backup-before-rebase. After any force-push that feels even slightly off, run git reflog immediately.
The Detached HEAD Era. Detached HEAD is when you check out a commit that is not at the tip of any branch. Git lets you make commits in this state, but those commits are not on any branch. Move HEAD, and the commits become unreachable except through the reflog.
For about a year I made commits in detached HEAD state without realizing what detached HEAD meant. I ignored Git’s warning because the warning was long and felt like an error message rather than information. The commits I made became orphaned every time I switched branches.
This is a stupid mistake. It is also one I made repeatedly because I never read what detached HEAD actually meant.
The fix is to read Git warnings. The detached HEAD warning is one of the most important ones, and most engineers ignore it for years.
The git add . Tax. git add . stages everything in the current directory. Senior engineers use it because it is fast. The cost is the things that quietly get staged that you did not intend to commit. The .env file you copied in for local testing. The debug log from reproducing the bug. The IDE config with your personal paths. The build artifact that should be in .gitignore but is not.
Every senior engineer reading this has shipped at least one of those to production. The fix is git add -p. The -p flag walks you through every hunk and asks whether to stage it. Ten minutes the first time, two minutes after a few weeks of practice.
The deeper fix is git diff --staged before any commit to a shared branch. Read every line. Not skim. Read. Five seconds catches more bugs than any code review.
The audit you should run this week
Five commands. Five minutes total.
git reflog
git stash list
git fsck --lost-found
git log --all --oneline | head -20
git diff --staged
If git reflog looks unfamiliar, you have Reflog Amnesia. If git stash list has more than three entries you cannot identify, Stash Graveyard. If git fsck --lost-found shows dangling commits, Force-Push Forgiveness. If git log --all shows commits without branch references, Detached HEAD Era. If git diff --staged shows you things you did not mean to commit, the git add . Tax.
The five mistakes feed each other. Senior engineers with Reflog Amnesia tend to also have Force-Push Forgiveness because they do not know the recovery path exists. The connection to the TanStack attack is the same: every trust assumption you cannot verify is a trust assumption that can break in ways you will not see until it is too late.
TanStack’s release workflow trusted a cache. Your local Git workflow trusts a state. Both assumptions are correct until they are not.
The fix is not memorizing more Git commands. The fix is treating Git as a timeline you can read, not a black box you push code into. Once you can read the timeline, every trust assumption becomes a verifiable claim instead of a hope.
I still make Git mistakes. The ones I make now take seconds to fix instead of hours. That is the difference between trusting Git and using it.
If you want more on what happened to TanStack at the workflow level, the official TanStack postmortem at tanstack.com/blog/npm-supply-chain-compromise-postmortem is the cleanest writeup I have read. And if you want the deeper Git knowledge most engineers skip, the official git reflog documentation at git-scm.com is worth twenty minutes.
I write about production mobile engineering with receipts. Follow if that is useful. ❤
메타데이터
- post_id
- e29e71cdf2f0
- slug
- the-openai-npm-attack-was-a-git-mistake-you-probably-made-it-too-e29e71cdf2f0
- url
- https://medium.com/@simra.cse/the-openai-npm-attack-was-a-git-mistake-you-probably-made-it-too-e29e71cdf2f0
- canonical_url
- https://medium.com/@simra.cse/the-openai-npm-attack-was-a-git-mistake-you-probably-made-it-too-e29e71cdf2f0
- author_url
- https://medium.com/@simra.cse
- status
- ok
- fetched_at
- 2026-06-09 14:34:10