On 27 May 2026 at 18:53 UTC, a configuration file called opencode.json was committed to LiteLLM's public LiteLLM-Labs/lite-harness repository. It held an administrative key to an internal service. The mistake was spotted quickly: about three hours later a second commit deleted the file, and the key was noted as needing revocation. Nobody revoked it [1].
On 10 October, 136 days later, an attacker used that key. Eleven days after that, LiteLLM's own release pipeline built, signed and published a malicious litellm==1.107.0 to PyPI and to its container registries [1].
LiteLLM's canonical record of the incident, published on 23 October, lists the stolen credentials, the timestamps and the steps in order [1]. Read in order, it is less a story about a clever attacker than a chain of ordinary decisions, each reasonable on the day it was made. Two of them are specific to teams that build agents, and those are the two worth checking in your own setup this week.
Deleting the file did not delete the key
The follow-up commit removed opencode.json from the repository's current files. It did not remove the commit that added it. That commit "stayed in the repository's public history until October 22" [1], roughly 147 days in all.
This is how Git works, and it takes three commands to see it. We reproduced the pattern in a throwaway repository with a fake key: one commit adds a config file, the next deletes it, and the working tree looks clean.
# Which files were ever deleted, and in which commit?
git log --all --diff-filter=D --name-only --format='%h %s'
# Print a deleted file as it was just before the deleting commit
git show <deleting-commit>^:opencode.json
# Every commit that added or removed a given string
git log --all -S'ADMIN_KEY' --format='%h %s'
The second command printed our fake key back in full. Anyone who cloned the public repository during those 147 days could do the same.
LiteLLM names the cause plainly: "Because of human error, nobody followed through on revoking the administrative key" [1]. Its fix is as much process as tooling. Secret-scanning alerts will open tickets automatically, "and the ticket is only closed when revocation is confirmed" [2]. The point is the closing condition. A leak that is noticed, deleted and written down still counts as open until the key stops working.
An agent platform is a credential store with a list endpoint
The key opened the LiteLLM Agent Platform, which the record describes as "an experimental project for self-hosted cloud agents to file PRs to GitHub repositories" [1]. To open pull requests, the platform needed a GitHub token. The one it was given was not a personal access token scoped to the right repositories. It was the GitHub CLI OAuth token of one of the company's two GitHub organization owners [1].
At 21:27:10 UTC on 10 October, a single request, GET /api/v1/managed_agents/agents?limit=100, returned the configuration of every managed agent. That response contained the owner's token [1].
The record describes no exploit at this step, only authenticated requests made with the leaked key [1]. An agent's configuration includes the credentials the agent acts with, so an endpoint that lists configurations lists credentials. Any platform that runs agents on someone's behalf holds the same kind of bundle, and the read and list paths are where it leaks.
The kind of token made it worse. LiteLLM's record notes that GitHub CLI tokens "are not scoped, are post-2fa, and not subject to GitHub's 'super-user' 2fa reauth flows" for sensitive organization or repository actions [1]. From that request onward, the attacker could act as an organization owner.
If you read an early account, this part changed. LiteLLM's first post, written during the response, suspected the tokens came from the logs of an internal LiteLLM deployment [1][3], and Wiz's report carried that statement [4]. The canonical record corrects it: the request logs show the first token came from the Agent Platform [1]. LiteLLM left the early text in place and marked it [3].
Eleven days with the owner's keys
From the owner's token to the malicious release took ten and a half days. The record's timeline, trimmed to the steps that mattered [1]:
| When (UTC) | What happened | Credential used |
|---|---|---|
| 27 May, 18:53 | opencode.json with the platform's admin key is committed to a public repository |
none |
| 10 Oct, 21:27 | The agent list endpoint returns the owner's GitHub CLI token | platform admin key |
| 10 Oct, 22:11 to 22:12 | Two throwaway branches run workflows that post CI secrets to webhook.site, 24 seconds apart; two more maintainers' tokens are taken | owner's token |
| 10 Oct, 22:14 | Both workflow runs are deleted | owner's token |
| 12 Oct | A second round of exfiltration across private repositories | owner's token |
| 21 Oct, 10:08:29 | A push changing the release workflow's source is rejected by branch protection | second maintainer's token |
| 21 Oct, 10:08:34 | The same change goes in, overriding branch protection | owner's token |
| 21 Oct, 10:46 | The staging build runs from the lookalike fork Berri-AI/litellm |
owner's token |
| 21 Oct, 11:11 | The production gate is approved | second maintainer's token |
| 21 Oct, 11:39 to 11:40 | Seven litellm==1.107.0 wheels reach PyPI |
none |
| 21 Oct, 11:53 to 11:54 | The production and staging run records are deleted | third maintainer's token |
| 21 Oct, 12:06 | An outside researcher opens the issue identifying the compromise | none |
| 21 Oct, by 13:23 | PyPI quarantines the release | none |
Three things in that table deserve a second look.
The rejection and the override are five seconds apart. Branch protection did its job. It required a pull request, two passing checks and the codeowner's approval, and it rejected the push [1]. The owner's token was simply allowed to override it.
The release gate had been loosened that week. A release normally had to be started by an authorized engineer and approved by "a special account locked to a physical device in our office" [1]. Earlier that week the rule was relaxed for a push toward more frequent releases, so that any two engineers could trigger and approve a release [1]. The attacker held two engineers' tokens. LiteLLM adds, fairly, that an owner could have removed the restriction anyway, and believes the attacker used separate tokens to keep the audit log quiet [1].
The malicious release was validly signed. LiteLLM's later post describes version 1.107.0 as having a valid signature and valid Trusted Publishing provenance, and states that "no pipeline controls can defend against an attacker who is an organization owner, because the owner owns the pipeline" [2]. A signature proves which pipeline built an artifact. It says nothing about who controlled that pipeline at the time.
The damage window was short by supply chain standards. The PyPI wheels were live from 11:39 to 13:23 UTC, and the affected Docker tags from 11:46 to 14:20 UTC [3]. Wiz, citing ClickPy data, counts approximately 49,000 downloads of the malicious version [4]. The payload ran on every Python start, collected cloud, browser and AI provider credentials, and queried the LiteLLM proxy's own credentials table [4]. Wiz observed stolen AWS credentials being tested about four and a half hours after exfiltration, including Bedrock model listing calls it describes as consistent with LLMjacking [4].
Detection came from outside. Lidor Machluf of Upwind Security opened the public issue 27 minutes after the first wheel went up, and LiteLLM's incident response began at 13:02 [1].
Not a sequel to March
Readers of our earlier coverage will remember that LiteLLM was compromised in March as well. The two incidents are not connected. LiteLLM's 30 October post says they "look similar from the outside" but went through different surfaces [2]:
| March 2026 | October 2026 | |
|---|---|---|
| Way in | A compromised dependency (Trivy) running in CI | A leaked admin key for an internal experimental service |
| What the attacker held | Static release credentials from CI environment variables | An organization owner's GitHub token, then more tokens |
| How the package got out | Published directly with stolen keys | Built, signed and published by the normal pipeline |
The controls LiteLLM built after March, including Trusted Publishing, signed images, immutable release tags and pinned GitHub Actions, worked as designed and limited the damage: existing release tags could not be overwritten [2]. Their goal was to keep untrusted code and long-lived credentials away from the release path [2]. October came in through an owner account instead.
We mapped the 25 commitments onto the chain
LiteLLM's 30 October post ends with 25 numbered commitments, each with a status and a target date, and a promise to update the table in place [2]. We counted the statuses as published on 8 November: 3 complete, 13 in progress, 9 planned. Then we matched each commitment to the step of the attack it would have broken. The matching is our reading; where LiteLLM ties a commitment to a step itself, the table says so.
| Step in the attack | Commitment that breaks it | Status on 8 Nov |
|---|---|---|
| Key committed, never revoked | #10 secret scanning and push protection on every repository; #11 ticketed revocation (LiteLLM ties it to the May key) | In progress, 30 Nov; Planned, 30 Nov |
| Exposed platform hands over a token | #12 identity-aware proxy in front of internal deployments; #24 automations never authenticate as a person | In progress, 30 Nov; In progress, 31 Jan 2027 |
| That token belongs to an owner | #2 dedicated owner accounts that never use tokens, OAuth apps or the gh CLI | Complete |
| CI secrets taken through throwaway branches | #25 secrets only in environments limited to protected branches (LiteLLM: would have stopped the extra token theft, if the stolen token was not an owner's) | Planned, 31 Jan 2027 |
| Owner token overrides branch protection | #18 rulesets on main and the release repository with no bypass actors; #4 alert on any owner-token activity | Planned, 31 Dec; In progress, 15 Nov |
| Stolen token approves the release | #1 release approval held only by the device-bound account | Complete |
| Run records deleted, history force-pushed | #3 audit logs streamed to Splunk with alerting; #13 alerts on every action the attacker took | Complete; In progress, 30 Nov |
| Bad artifact already published | #19 to #21 a verifier outside GitHub that compares releases with the tag and yanks on mismatch | Planned, 31 Dec |
Read from the bottom, the order makes sense. The three completed items sit at the end of the chain: who can be an owner, who can approve a release, and whether anyone sees what an owner does. Those are the steps that turned a leaked key into a poisoned package, and closing them first limits the worst outcome. Commitment #1 is not really a new control, though. It largely restores the device-bound approval rule that was relaxed the week of the incident [1].
Read from the top, the picture is less finished. The first link, a key that was found, deleted and never revoked, is closed by #10 and #11, and on 8 November neither was complete. A team cannot fix everything at once, and LiteLLM published this table with dates and a promise to say when items slip, which is more than most companies do after an incident. But the cheapest step in the whole chain to break was the first one.
What to check in your own agent setup
- Revoke, then delete. Treat an exposed key as live until revocation is confirmed, and run the three commands above on any repository that has ever been public.
- Never give an agent a person's token. Use a GitHub App or a fine-grained, expiring token with the smallest scope that works, which is now LiteLLM's rule for every automation [2]. An owner's CLI token is the worst possible choice: unscoped, past 2FA, and able to override protections [1]. We have covered giving each agent its own identity before.
- Treat agent configuration as a secret store. Any endpoint that lists or reads agent configurations should return credentials redacted, or never return them at all. This is our recommendation, drawn from step 1, not a LiteLLM commitment.
- Size the account to the agent. LiteLLM's new policy assumes that "everything on a developer's machine should be assumed readable by the agents running on it", so regular users may use agents but are never owners or admins [2].
- Keep experimental services off the public internet. The platform that leaked the token was an experimental project [1]. LiteLLM is putting every internal deployment behind an identity-aware proxy, already done for most systems [2].
- Verify releases from outside the pipeline. Signatures and provenance prove origin. A verifier running outside the pipeline, comparing each published artifact with the tagged source, catches a pipeline that was told to build the wrong source. LiteLLM plans one by 31 December [2].
If you installed 1.107.0. You are affected if you installed an unpinned LiteLLM with pip between 11:39 and 13:23 UTC on 21 October, or ran the
v1.107.0,latest,main-latestormain-stableimages between 11:46 and 14:20 UTC [3]. LiteLLM's cleanup steps start with a warning not to runpip installfirst, because any Python start re-runs the payload; then reinstall a clean pinned version and rotate everything the host could reach [3]. If you verify LiteLLM images with cosign, note that the signing key was replaced on 23 October, and releases from 24 October use the new key [3].
The attack needed three things: a key nobody revoked, a token nobody scoped, and a release gate loosened for a busy week. None of them was exotic. The first is a ticket that was never closed, the second is a shortcut that is easy to take in an agent prototype, and the third is a decision a team makes when it is trying to ship faster. That is the uncomfortable part. Every one of them can happen on a team that is doing most things right.
References
[1] Oliver Jensen and Mateo Wang, LiteLLM: The attack chain behind the v1.107.0 LiteLLM release (2026-10-23). Blog
[2] Oliver Jensen, Krrish Dholakia and Ishaan Jaffer, LiteLLM: What we are changing after October 21 (2026-10-30). Blog
[3] LiteLLM: Contained: Oct 21, 2026 LiteLLM release v1.107.0 Compromised (2026-10-21). Blog
[4] Wiz Research: LiteLLM Compromised, Steals Credentials (2026-10-21). Blog