LiteLLM's own incident post says the attacker behind the poisoned 1.107.0 release held three developers' GitHub CLI tokens, most likely taken from the logs of an internal LiteLLM gateway that was reachable from the internet. Anyone running an AI gateway should treat its logs as a secret store, check them for tokens, and tighten GitHub release controls.
This post is the post mortem view. If you need the indicators, the cleanup order and the rebuild steps for the malicious package itself, start with our first write up, LiteLLM 1.107.0 PyPI compromise. If you want the general case for and against running a gateway, read Is an LLM gateway a security risk?. Here we follow the credential chain LiteLLM described, link by link, and look at which control would have broken each link. Most of those controls live in two places teams rarely review together: the logging settings of an AI gateway, and the bypass settings of a GitHub organization.
One point of care before we start. The claim that Claude Code put GitHub CLI tokens into response bodies comes only from LiteLLM. As of 22 October we found no public statement from Anthropic about it, LiteLLM has not described the "specific situations" it refers to, and LiteLLM itself says the theft of tokens from its logs is what it believes happened, not what it has proven. We treat it as LiteLLM's finding, attribute it each time, and do not describe it as a confirmed Claude Code vulnerability. Everything we say about how Claude Code talks to a gateway comes from Anthropic's own documentation.
What happened
On 21 October 2026, LiteLLM's release pipeline built and published version 1.107.0 to PyPI, Docker Hub and the GitHub Container Registry (GHCR) from a lookalike repository, Berri-AI/litellm, instead of the real BerriAI/litellm. According to LiteLLM, every wheel and the affected container tags carried compiled malware that steals credentials and loads remote code. The official v1.107.0 source tag did not contain the malicious files.
What makes this incident worth a separate post is LiteLLM's account of how the attacker got the access to do that. In its 21 October post, last updated at 20:30 UTC, LiteLLM says:
- After its March 2026 incident it had hardened its release process, and on Monday 19 October it "temporarily expanded release permissions" for two internal engineers so they could ship daily bugfix backports for customers.
- The attacker held GitHub CLI credentials for three developers. One of those credentials could run workflows, and one belonged to one of LiteLLM's two GitHub organization owners.
- The organization owner's token was used to override branch protection and force the workflow change through. A second developer's credential satisfied the production approval gate. A third deleted the workflow run records afterwards.
- LiteLLM found "an unexpected, innocuously named user account" on its internal LiteLLM deployment, which it says was "exposed to the public internet." It believes the attacker got into that deployment, possibly through "a recently patched vulnerability" that it has not confirmed, and took the GitHub CLI tokens from the deployment's logs.
- On where the tokens came from, LiteLLM writes that it has observed "specific situations in which Claude Code will embed a user's GitHub CLI OAuth token into a response body," and that where response bodies are logged, those tokens "may appear in LiteLLM logs."
The Hacker News reported the same account on 22 October and noted that LiteLLM had not explained how the deployment became reachable from the internet. Its article contains no comment from Anthropic. The Register's 21 October piece says BerriAI did not respond to its questions at the time.
Timeline
All times are UTC. The source for each row is in the last column. Where two sources give different times for what looks like the same event, both are shown.
| Date and time (UTC) | Event | Source |
|---|---|---|
| 13 Oct 2026 | Domain litellm.dev registered; GitHub account letters9813 created | GitHub issue #47826 reporter |
| 19 Oct 2026 (Monday) | Release permissions temporarily expanded for two engineers doing bugfix backports | LiteLLM incident post |
| 20 Oct, 09:15 | Dead drop file domain.txt emptied or left empty | GitHub issue #47826 reporter |
| 21 Oct, 10:08:29 | Branch protection rejects an unauthorized change to the release workflow | LiteLLM incident post |
| 21 Oct, 10:08:34 | A privileged developer credential overrides protection and applies commit 9cf97e935aeaf791b9c8db224a2bb3ee156134ce | LiteLLM incident post |
| 21 Oct, 10:46:39 | Staging run starts, building from Berri-AI/litellm at commit 20535d8fa11f786a6c24df7488a0e449ed5f09ad | LiteLLM incident post |
| 21 Oct, 11:08:09 | Staging run completes | LiteLLM incident post |
| 21 Oct, 11:08:53 | Production pipeline starts | LiteLLM incident post |
| 21 Oct, 11:11:18 | A second developer credential satisfies the production approval gate | LiteLLM incident post |
| 21 Oct, about 11:21 | Wheels built by maturin | GitHub issue #47826 reporter |
| 21 Oct, 11:39:51 to 11:40:13 | Seven malicious wheels uploaded to PyPI | LiteLLM incident post; issue #47826 |
| 21 Oct, 11:46:56 | Container publication runs with malicious content | LiteLLM incident post |
| 21 Oct, 11:50:24 | GitHub release published | GitHub issue #47826 reporter |
| 21 Oct, 11:53:49 to 11:54:01 | A third developer credential deletes production and staging workflow run records | LiteLLM incident post |
| 21 Oct, 12:06:41 | Public GitHub issue #47826 opened | GitHub issue #47826 |
| 21 Oct, 12:09:38 to 12:24:52 | Unauthorized workflow revision removed; release workflow restored | LiteLLM incident post |
| 21 Oct, by 13:23 | PyPI removes litellm==1.107.0 | LiteLLM incident post; issue #47826 |
| 21 Oct, about 13:30 | GitHub takes down the dead drop file | The Hacker News |
| 21 Oct, 13:51 | GHCR tags found unavailable; Docker Hub tags still point to the compromised index | LiteLLM incident post |
| 21 Oct, 14:20 | All affected Docker Hub tags removed or repointed | LiteLLM incident post |
| 21 Oct, 16:20 to 17:01 | Stolen AWS instance role credentials validated from two IP addresses | Researchers cited by The Hacker News |
| 21 Oct, 17:13 | Issue status update says Docker tags yanked | GitHub issue #47826 |
| 21 Oct, 19:22 | The Register publishes its report | The Register |
| 21 Oct, 20:30 | LiteLLM's latest update, naming Claude Code and gateway logs as the suspected token source; next update promised for 22:30 | LiteLLM incident post |
| 22 Oct, 09:42:54 | Last update to GitHub issue #47826 at time of writing | GitHub issue #47826 |
| 27 Oct (scheduled) | Disclosure date for a vulnerability fixed in an embargoed enterprise release | LiteLLM incident post |
Two gaps in the table are worth noticing. First, the override at 10:08:34 came five seconds after branch protection rejected the change at 10:08:29, and the staging run against the lookalike repository started 38 minutes later. Five seconds suggests someone who already knew which credential would get past the rule. Second, 1 hour and 43 minutes passed between the first wheel landing on PyPI and its removal. For anyone who installs unpinned, that is plenty.
What changed since first reports
The first public reports, including the GitHub issue opened at 12:06 UTC and the first Hacker News article on 21 October, said the release was built from a lookalike repository and that the source of the compromised credential was unknown. LiteLLM's director of security told The Hacker News that day that three engineers' accounts were used, apparently through stolen GitHub CLI OAuth tokens, and that all involved tokens had been revoked.
The 20:30 UTC update added the parts this post is about: the internet reachable internal deployment, the unexpected user account, the suspected unconfirmed vulnerability, and LiteLLM's statement about Claude Code and response bodies. The GitHub issue reporter also retracted an earlier claim that the GitHub release appeared ten minutes before the PyPI upload, calling the ordering normal for the project.
Technical details
This section collects the defender useful values from the primary sources. Every value below was read in a fetched source and is attributed. Long values are in backticks. We do not reproduce any part of the payload.
Affected artifacts
| Artifact | Affected | Window (UTC, 21 Oct) | Source |
|---|---|---|---|
PyPI litellm | 1.107.0, all seven wheels | 11:39:51 to 13:23 | LiteLLM incident post |
PyPI litellm sdist | litellm-1.107.0.tar.gz, no payload files, tampered pyproject.toml | same | GitHub issue #47826; The Hacker News |
Docker Hub litellm images | Tags v1.107.0, latest, main-latest, main-stable | 11:46:56 to 14:20 | LiteLLM incident post |
GHCR ghcr.io/berriai/litellm | Same tags | 11:46:56 until unavailable by 13:51 | LiteLLM incident post |
GHCR ghcr.io/berriai/litellm-database | Same tags | same | LiteLLM incident post |
GHCR ghcr.io/berriai/litellm-non_root | Same tags | same | LiteLLM incident post |
| LiteLLM private enterprise release | Not affected | n/a | LiteLLM incident post |
GitHub source tag v1.107.0 | Does not contain the malicious files | n/a | LiteLLM incident post |
The safe pin LiteLLM gives is litellm==1.106.2. The issue reporter lists these releases as checked and clean: all wheels of 1.103.6, 1.103.7, 1.104.4, 1.104.5, 1.105.2, 1.105.3, 1.106.0, 1.106.1, 1.106.2, 1.107.0rc1 and 1.107.0rc2; litellm-proxy-extras 0.4.108 to 0.4.110; and litellm-enterprise 0.1.69.post2, 0.1.73.post1 and 0.1.76.post1. The reporter did not inspect the Docker images.
Image digests
| Image | Digest | Source |
|---|---|---|
ghcr.io/berriai/litellm and the Docker Hub tags | sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e (index) | LiteLLM incident post |
ghcr.io/berriai/litellm-database | begins sha256:c00f9d92c427 | LiteLLM incident post |
ghcr.io/berriai/litellm-non_root | begins sha256:7b00e91cd2fe | LiteLLM incident post |
For the two digests shown as prefixes, take the full value from LiteLLM's post before you match on it.
Wheel and sdist hashes
These SHA-256 values come from the reporter of GitHub issue #47826. File names are as listed by The Hacker News.
| File | SHA-256 |
|---|---|
litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl | 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d |
litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl | 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a |
litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl | 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 |
litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl | 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 |
litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whl | c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 |
litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl | 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 |
litellm-1.107.0-cp310-abi3-win_amd64.whl | 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 |
litellm-1.107.0.tar.gz | 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 |
Repository and commit identifiers
| Item | Value | Source |
|---|---|---|
| Real repository | BerriAI/litellm | LiteLLM incident post |
| Lookalike build source | Berri-AI/litellm | LiteLLM incident post |
| Workflow change applied after the override | 9cf97e935aeaf791b9c8db224a2bb3ee156134ce | LiteLLM incident post |
| Lookalike commit used by the staging run | 20535d8fa11f786a6c24df7488a0e449ed5f09ad | LiteLLM incident post |
Commit behind the official v1.107.0 tag | 66c8f5b1a9cefaa7c55b758f86f4ab552f2e859e | GitHub issue #47826 reporter |
| Build change in the sdist | data = "cook" under [tool.maturin] in pyproject.toml, absent from the tagged commit | GitHub issue #47826 reporter |
Indicators of compromise
| Type | Indicator | Source |
|---|---|---|
| File | site-packages/cook.pth, cook.py, sitecustomize.py, plus __pycache__/cook*.pyc and sitecustomize*.pyc | LiteLLM incident post |
| File | litellm-1.107.0.dist-info | LiteLLM incident post |
| Lock file | /tmp/remote_code_loader.lock, held by a detached Python daemon | LiteLLM incident post |
| Marker | /tmp/.pc_once_72129118c59f5636c0bb, written only after a successful exfiltration | GitHub issue #47826 reporter |
| Temp dirs | /tmp/.pc_sq_* and /tmp/.pc_e_* | GitHub issue #47826 reporter |
| Environment | PRIVCOLLECT_ACTIVE=1 | GitHub issue #47826 reporter |
| Process | Orphaned Python process with working directory / | GitHub issue #47826 reporter |
| Network | POST http://www.litellm[.]dev/healthcheck | GitHub issue #47826 reporter; The Hacker News |
| HTTP headers | X-Token: XWiA8gZNa7NcX2XRoAEb, X-SHA256, Content-Type: application/octet-stream, fixed MSIE 7.0 user agent | GitHub issue #47826 reporter |
| Dead drop | GitHub user letters9813, repo letters9813/pythonclient-test, file domain.txt | GitHub issue #47826 reporter |
| Second stage path | /codepy on a domain read from the dead drop | The Hacker News |
| Crypto | AES-256-CBC with an RSA-4096 OAEP wrapped key; key begins MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU | GitHub issue #47826 reporter; The Register |
| Source IPs (cloud credential abuse) | 168.100.8[.]57, 165.227.201[.]8 | Researchers cited by The Hacker News |
| Cloud API pattern | sts:GetCallerIdentity, then bedrock:ListFoundationModels and a ListModels call; in some cases ListRoles, ListUsers, ListAccessKeys, ListAccounts, DescribeTrails | Researchers cited by The Hacker News |
The issue reporter says the second stage was dormant because the dead drop had been empty since 20 October, and that on Windows the sitecustomize loader fails silently while cook.py still runs. Researchers cited by The Hacker News said domain.txt held only test values before the takedown, and that credential abuse began about four and a half hours after exfiltration.
The credential chain, as LiteLLM describes it
Put the pieces in order and the incident is a chain of six links. Each one is a separate control that could have stopped it.
| Link | What LiteLLM says happened | Control that breaks this link |
|---|---|---|
| 1. Token enters model traffic | In "specific situations," Claude Code embedded a user's GitHub CLI OAuth token in a response body | Keep long lived tokens out of agent sessions; sandbox agent shells; short lived tokens |
| 2. Gateway writes it to disk | Response bodies were logged by the internal LiteLLM deployment | Do not log message content by default; redact token patterns in the log copy |
| 3. Logs are reachable | The internal deployment was exposed to the public internet; an unexpected user account appeared | Private network only; SSO; patch the gateway; alert on new users |
| 4. Token is powerful | One token belonged to an organization owner; release permissions had been widened on 19 Oct | No owner tokens on laptops; time box and auto revert widened permissions |
| 5. Rules can be bypassed | The owner credential overrode branch protection five seconds after a rejection | "Do not allow bypassing the above settings"; rulesets with an empty bypass list |
| 6. Approval is a credential, not a person | A second stolen credential approved production; a third deleted run records | Prevent self review; hardware backed approvals; audit log streaming outside GitHub |
The rest of this post works through those links.
How it works, in plain language
An LLM gateway sits between your developers' tools and the model provider. Anthropic's own gateway documentation lists what a gateway gives an organization: the provider key stays on the server, usage is attributed per developer, budgets are enforced, and "Audit logging: log every model request for compliance." That last line is the important one for this incident. A gateway that logs every request and response is, by design, a recording of everything every connected tool sent and received.
A coding agent's traffic is not just prompts. When an agent runs a shell command, reads a file or calls a tool, the output becomes part of the conversation that goes back and forth with the model. If a command prints a token, or a file contains one, that token can travel through the gateway as conversation content. LiteLLM's statement says it saw a GitHub CLI token in a response body, and that its logs picked it up there. LiteLLM has not said which side of which exchange that was.
Anthropic's documentation describes what Claude Code sends to a gateway at the protocol level. Requests go to /v1/messages. The Authorization or x-api-key header carries the developer's gateway credential. An x-claude-code-session-id header lets the gateway group a session's requests without parsing bodies. Optional routing hint headers carry, in Anthropic's words, "never prompt text or file contents." None of that is unusual. The point is that the bodies themselves carry whatever the session contains, and the gateway decides whether to keep them.
Now add the GitHub CLI. Its manual says gh auth login stores the token in the system credential store, and falls back to "writing the token to a plain text file" if no store is available. GitHub's documentation says OAuth access tokens start with gho_. LiteLLM describes the leaked credentials as GitHub CLI OAuth tokens. Such a token acts as the user who signed in, within the scopes granted. If that user is an organization owner, the token can do owner things.
Finally, GitHub's defaults. GitHub's branch protection documentation says restrictions do not apply to repository admins by default, and "People and apps with admin permissions to a repository are always able to push to a protected branch." Deployment environments let administrators "bypass configured protection rules" by default too. An owner token can therefore change a protected release workflow unless someone has turned that bypass off.
Chain those together and you get LiteLLM's sequence: a token appears in agent traffic, a gateway logs it, the logs are reachable, the token belongs to an owner, the owner can bypass protection, and a second stolen credential counts as the second approver.
Who is affected
| Group | Why it matters | What to do first |
|---|---|---|
Anyone who installed litellm==1.107.0 or pulled the four affected tags in the windows above | The payload ran on every Python start | Follow the cleanup in our first LiteLLM post |
| Teams running any LLM gateway (LiteLLM or another product) with request or response logging on | The logs may hold tokens your coding agents touched | Search the logs for token prefixes, then restrict and purge |
| Teams whose developers use AI coding agents with the GitHub CLI signed in | Any agent session can see what the shell can see | Move to short lived, narrowly scoped tokens; sandbox agent shells |
| GitHub organizations where admins or owners can bypass branch protection or environment rules | One owner token is enough to change a release workflow | Turn on "Do not allow bypassing"; review ruleset bypass lists |
| Projects that publish to PyPI or container registries from GitHub Actions | Signed, trusted publishing still published malicious artifacts here | Pin the source repository identity in the workflow and verify it |
Detection: what to check this week
These steps assume you have already handled any install of the bad release itself. They focus on the post mortem lessons: tokens in gateway logs, GitHub audit trails and release workflow drift. Commands are for Linux and macOS shells unless marked; adapt paths to your own setup.
1. Find every LiteLLM install and image without starting Python
LiteLLM's post warns that starting Python in an affected environment re-runs the .pth file. So do not run pip, python -c or anything that imports. Use the filesystem.
# Hosts and venvs: look for the bad dist-info and the payload files
sudo find / -xdev \( -name 'litellm-1.107.0.dist-info' -o -name 'cook.pth' -o -name 'remote_code_loader.lock' \) 2>/dev/null
# Lockfiles and requirements in your repos
grep -rnE 'litellm.{0,6}1\.107\.0' --include='requirements*.txt' --include='*.lock' --include='pyproject.toml' .
# Local Docker images: match the published index digest
docker image ls --digests | grep 1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e
# What a moving tag points to right now (read only)
docker buildx imagetools inspect ghcr.io/berriai/litellm:main-stable
# Kubernetes: which running pods use a litellm image, and by which digest
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{range .status.containerStatuses[*]}{.imageID}{" "}{end}{"\n"}{end}' | grep -i litellm
On Windows hosts, search the same names with PowerShell:
Get-ChildItem -Path C:\ -Recurse -Force -ErrorAction SilentlyContinue -Include 'cook.pth','cook.py','litellm-1.107.0.dist-info' | Select-Object FullName
2. Search your own gateway logs for GitHub tokens, without printing them
This is the step most teams have not done. Whatever gateway you run, find where it writes request and response content: local files, a database table, a log shipper, a tracing backend, an observability callback. Then search for GitHub token prefixes. GitHub documents the prefixes as ghp_ (classic personal access token), github_pat_ (fine grained token), gho_ (OAuth access token), ghu_ (GitHub App user token), ghs_ (installation token) and ghr_ (refresh token).
List matching files and counts only. Do not print the matches into your terminal history or a ticket.
# Count token-shaped strings per file, never echo the values
grep -rEc 'gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}' /var/log/your-gateway/ 2>/dev/null | grep -v ':0$'
If your gateway logs to Postgres or another database, run the equivalent pattern query against the columns that hold message or response content, again returning counts per day rather than values. Do the same in any external log or trace store you forward to.
Any hit means that token must be treated as exposed, whether or not the gateway was ever reachable from outside. Revoke it at GitHub first, then purge the log entries, then look at who could read those logs.
3. Check how your gateway is exposed and who has accounts on it
LiteLLM's post names two findings on its own deployment: it was reachable from the public internet, and it had an "unexpected, innocuously named user account." Check both on yours.
# From a machine outside your network: does the admin UI or API answer?
curl -s -o /dev/null -w '%{http_code}\n' https://your-gateway.example.com/
# Inside: which listeners are bound to all interfaces?
sudo ss -tlnp | grep -E '0\.0\.0\.0|\[::\]'
Then list user accounts in the gateway's admin interface and compare them with your identity provider. Any account you cannot tie to a person or a service is an incident until proven otherwise. LiteLLM also says an embargoed fix for a vulnerability is scheduled for disclosure on 27 October; it did not say whether that is the vulnerability it suspects in its own intrusion. Plan to patch your gateway promptly when that disclosure lands, and keep the gateway off the internet in the meantime.
4. Read the GitHub audit log for the moves in this incident
GitHub records each step of the chain LiteLLM described. In the organization audit log, search for these events, which GitHub documents:
| Event | What it records | Why it matters here |
|---|---|---|
protected_branch.policy_override | A repository administrator overrode a branch protection requirement | The 10:08:34 override |
protected_branch.rejected_ref_update | A branch update attempt was rejected | The 10:08:29 rejection that preceded it |
protected_branch.destroy | Branch protection was disabled | A cruder version of the same move |
protected_branch.update_admin_enforced | Enforcement for administrators changed | Someone turning "Do not allow bypassing" off |
protected_branch.update_allow_force_pushes_enforcement_level | Force pushes enabled or disabled | LiteLLM says the change was force pushed |
repository_ruleset.update | A ruleset was edited, including bypass actors | Bypass lists quietly widened |
workflows.approve_workflow_job | A workflow job was approved | The 11:11:18 approval |
workflows.delete_workflow_run | A workflow run was deleted | The 11:53:49 deletions |
environment.update_protection_rule | A deployment protection rule, including approvers, was updated via the API | Approver lists changed before a release |
The important lesson in the last two rows is that deleting run records does not delete the audit log entry that says they were deleted. A deletion of release runs minutes after a release is a high signal alert on its own.
In the web interface, use the search box with filters such as action:protected_branch.policy_override or action:workflows.delete_workflow_run. If your plan exposes the audit log through the API, the GitHub CLI can query it:
gh api -X GET /orgs/YOUR-ORG/audit-log -f phrase='action:workflows.delete_workflow_run' --paginate
gh api -X GET /orgs/YOUR-ORG/audit-log -f phrase='action:protected_branch.policy_override' --paginate
Stream the audit log to storage your GitHub owners cannot edit, so the record survives an owner token being stolen.
5. Diff your release workflow against a known good copy
The attacker's change at 10:08:34 pointed the release workflow at a different repository. Check yours for anything that builds from a repository other than your own, and for changes since your last reviewed release.
# Every repository reference in workflow files
grep -rnE 'repository:|actions/checkout|git clone|github\.com/' .github/workflows/
# What changed in workflows since the last release tag
git log --oneline v1.0.0..HEAD -- .github/workflows/
git diff v1.0.0..HEAD -- .github/workflows/
Replace v1.0.0 with your last release tag. Any repository: input on a checkout step in a release job should name your own organization exactly.
Remediation: break each link
Link 1: keep long lived tokens out of agent sessions
You cannot control what an agent prints if the shell it drives can print a powerful token. So reduce what that shell can reach.
- Do not keep an organization owner's GitHub CLI session signed in on a machine where an AI agent runs commands. Owners should use a separate account or profile for owner work.
- Prefer short lived credentials. The GitHub CLI manual describes an
--expiringoption forgh auth loginthat prefers expiring credentials gh refreshes automatically, where the host and OAuth app support it. For automation, the manual recommends a fine grained token inGH_TOKENover--with-token. - Give agent sessions their own narrowly scoped credential rather than your personal session. A fine grained token limited to one repository and read access cannot override branch protection anywhere.
- Use isolation that keeps credentials out of the agent's reach. Anthropic's Claude Code documentation describes a sandboxed bash tool with filesystem and network isolation, and permission deny rules for commands. It also describes cloud sessions where GitHub credentials "never enter the session VM": the VM holds a short lived credential scoped to that session and an Anthropic proxy attaches the real GitHub credential server side.
- Know where tokens rest. Anthropic says Claude Code stores its own API keys and tokens in the macOS Keychain when available and in a file with mode
0600on Linux. The GitHub CLI uses the system credential store and can fall back to plain text; rungh auth statusto see which you have. Neither protects a token once a command prints it.
gh auth status # shows the account, token storage location and scopes
gh auth logout # sign the CLI out on shared or agent machines
To revoke the token itself, remove the GitHub CLI authorization under your GitHub account's authorized OAuth apps, then sign in again with the narrower setup above.
Link 2: stop the gateway from keeping message content
Turn message logging off by default and opt in only where you have a specific need and a short retention period. For LiteLLM, the logging documentation gives these controls:
litellm_settings:
turn_off_message_logging: True # messages and responses not sent to logging providers; spend still tracked
redact_user_api_key_info: true # strips hashed token, user and team ids from supported loggers
global_disable_no_log_param: True # only if you need to stop clients skipping logging entirely
The same page documents a per request header, x-litellm-enable-message-redaction: true (marked beta), and an opt out header, LiteLLM-Disable-Message-Redaction: true, which the proxy honors only for keys or teams whose metadata includes allow_client_message_redaction_opt_out: true. Audit which keys carry that metadata. For OpenTelemetry, callback_settings.otel.message_logging: False turns off message content in traces. Check whether the proxy's own database logs hold content as well as your callbacks, using your version's documentation.
If you must keep content, redact token shaped strings in the copy you store. Anthropic's gateway compatibility guide is clear that a gateway "that rewrites or redacts request bodies for content inspection" breaks paired headers and fields, "so inspect without modifying." The lesson is to redact the log record, not the forwarded traffic. Your logging pipeline is the place for a pattern filter on gho_, ghp_, github_pat_ and your cloud providers' key formats.
Then purge. Old logs that hold message content are a liability whether or not you find tokens in them today.
Link 3: keep the gateway private and its accounts known
- Bind the admin UI and management API to a private network only. Put developer access behind SSO or a VPN.
- Alert on any new gateway user or key creation, and review accounts weekly against your identity provider.
- Patch the gateway on a schedule that matches its sensitivity. Anthropic notes that a gateway "becomes infrastructure your organization operates" and has to be kept updated as Claude Code evolves; the same is true for security fixes.
- On managed machines, Anthropic documents an
allowedProviderssetting: set it to["customEndpoint"]with the gateway'sANTHROPIC_BASE_URLin the managed settingsenvblock and Claude Code refuses sessions pointed anywhere else (Claude Code v2.1.285 or later). That keeps traffic on the gateway you secured instead of a developer's own proxy.
Our LLM gateway risk post covers network, runtime and key handling for a self hosted gateway in more depth.
Link 4: make powerful tokens rare and temporary
- Keep the number of organization owners small and keep owner credentials off daily driver machines.
- When you widen release permissions, as LiteLLM did on 19 October for a good operational reason, set an end date and an automatic revert. Write the change into the same ticket as the work it supports.
- Prefer GitHub App installation tokens (
ghs_) for automation. GitHub documents them as installation access tokens for a GitHub App, so they belong to a scoped app installation rather than to a person who may also be an owner.
Link 5: remove the bypass
- On every release branch, enable "Do not allow bypassing the above settings." GitHub says that applies restrictions to "admins and roles with the 'bypass branch protections' permission."
- For rulesets, keep the bypass list empty for release branches and workflow files, and alert on
repository_ruleset.update. - Keep force pushes blocked on protected branches. GitHub blocks them by default; check nobody turned them on.
- Protect
.github/workflows/with code owners and required review, so a workflow change needs a reviewed pull request even for admins.
Link 6: make the second approval a second human
GitHub's environment protection rules support required reviewers and "Prevent self-review," which stops the person who starts a deployment from approving it. GitHub also says administrators can bypass environment protection rules by default, and that you can disallow it. Turn that off for production.
Even with both settings on, LiteLLM's case shows the limit: two stolen credentials look like two people. Raise the bar further:
- Require hardware security keys for every account that can approve a production environment, so a token alone cannot approve.
- Add a wait timer to the production environment. GitHub supports 1 to 43,200 minutes, though on Free, Pro and Team plans only for public repositories. A short delay gives a human time to notice an unexpected run.
- Page a second channel, outside GitHub, when a release job is approved.
And at the publishing end: verify where the build came from
Our first LiteLLM post covers this in detail: the artifacts were signed and published through LiteLLM's real pipeline, and the provenance named the lookalike repository. A release job that checks the source repository's exact owner and name, ideally by numeric ID, before building would have refused Berri-AI/litellm. Our Tensorlake provenance post walks through the same check for npm.
Where sources differ
| Topic | What sources say |
|---|---|
| Start time | LiteLLM's overview says the push happened "At 4:00 AM PST"; its own minute timeline puts the PyPI upload at 11:39:51 UTC. Gigazine gives about 8:39 PM JST, which matches 11:39 UTC. Use the minute timeline. |
| Docker Hub cleanup | LiteLLM says all affected Docker Hub tags were removed or repointed by 14:20 UTC. The GitHub issue's status update saying Docker tags were yanked is stamped 17:13 UTC. Our earlier post gave about 13:53 to 14:32. Treat any pull of those tags before 17:13 as suspect if you cannot match the digest. |
| Token source | LiteLLM "believes" the tokens came from its gateway logs and says it observed Claude Code embedding tokens in "specific situations." Neither the situations nor the theft from logs are confirmed in public sources, and Anthropic has not commented publicly as far as we found. |
| Link to the March 2026 incident | The Hacker News's first article said the same .pth behaviour appeared in LiteLLM's March compromise. Its follow up cites researchers who found different embedded RSA keys and different exfiltration and backdoor behaviour, and no evidence linking the two, while not ruling out the same operator. |
| Trusted publishing | LiteLLM says it adopted trusted publishers after March. The issue reporter asked the maintainers to adopt PyPI trusted publishing with attestations, saying no release had published attestations so far. These may describe different stages or channels; we could not reconcile them. |
| Second stage | The issue reporter says the dead drop was empty since 20 October; researchers cited by The Hacker News say it held only test values. Either way, no second stage payload was publicly analysed. |
Open questions
- How did the attacker reach LiteLLM's internal deployment, and is the suspected "recently patched vulnerability" the one scheduled for disclosure on 27 October?
- In which "specific situations" does LiteLLM say Claude Code put a GitHub CLI token in a response body, and has that been reported to Anthropic? We found no Anthropic statement.
- How long were the tokens in the logs before the attack, and how far back did the unexpected user account exist?
- Did the attacker hold the three developer tokens before 19 October, and did the widened release permissions decide which token could approve production?
- What, if anything, did the attacker do with the GitHub accounts before the tokens were revoked? LiteLLM told The Hacker News it could not yet rule out further access.
- LiteLLM promised an update at 22:30 UTC on 21 October. We did not find a newer update on its post as of our last fetch.
What would have helped
Practices any team can use
- Treat AI gateway logs as a secret store. Message logging off by default, short retention when on, access limited to the people who already hold the provider keys, and a redaction filter on the stored copy.
- Assume agent sessions see everything the shell sees. Run agents with a separate, narrowly scoped GitHub credential, in a sandbox or a container, with deny rules for commands that print credentials.
- No owner tokens on agent machines. Separate owner work from daily development.
- Time box privilege changes. Every widening of release rights gets an expiry and an automatic revert.
- No bypass on release paths. "Do not allow bypassing" on branches, empty bypass lists on rulesets, administrators unable to bypass production environments.
- Approvals tied to people, not tokens. Hardware keys for approvers, "Prevent self-review," a wait timer where your plan supports it.
- Audit logs outside the blast radius. Stream GitHub audit logs somewhere owners cannot delete, and alert on
protected_branch.policy_overrideandworkflows.delete_workflow_run. - Consumers: pin by version and hash, images by digest, and wait. A cooldown of a few days before adopting a new release would have skipped this one entirely.
- Keep an inventory. Know which services run a gateway, which version, and which images, so the first question in an incident takes minutes. Our post on using SBOMs in incident response covers how.
Where Safeguard fits
Safeguard helps with the inventory and fix half of this incident. It would not have prevented it, and it does not decide how your gateway logs or how your GitHub organization grants bypass.
- Find every LiteLLM, everywhere. SBOM and dependency scanning covers repositories, container images and SBOM files. From the CLI:
safeguard scan --dir .for a repository,safeguard scan --image ghcr.io/your-org/your-gateway:tagfor an image, andsafeguard scan --sbom sbom.cdx.jsonfor an SBOM you already produce. Results are written as JSON under./output. In the platform you can see which projects contain a given component and version, which answers "where do we run litellm, and which version" across everything you have connected. - Vulnerability matching and reachability. Components are matched against vulnerability databases, and reachability analysis ranks findings by whether your code calls the affected paths.
- Policies and gates. A gate evaluated in the platform can block a merge or an image promotion that carries a component version you have flagged, so 1.107.0 cannot come back through a stale lockfile.
- Fix pull requests. Autonomous remediation can open pull requests that pin
litellm==1.106.2across repositories. It is off by default; you turn it on. - The assistant, with your approval. The Safeguard CLI's assistant can edit requirements files, pin image digests, rebuild and run tests, asking before each command in its default permission mode.
/permissionsswitches between Ask, Accept edits, Plan, Auto (default) and Strict. That approval model is the same principle this incident teaches about agents in general: the human sees each command before it runs. In CI,safeguard execruns the same work non interactively.
Where it does not help: Safeguard does not configure your gateway's logging, review who has accounts on it, change your GitHub branch protection or environment settings, or decide which credentials to revoke. Those stay with your team.
Zero-Day Discovery and Gold
- Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories before they reach public CVE databases. It covers seven package ecosystems.
- Gold Open Source at gold.safeguard.sh is free with no login: searchable CVE intelligence with CVSS, EPSS, KEV status, affected and fixed versions and a recommended safe version, a free read API, and a CI gate GitHub Action.
- Gold Registry at registry.safeguard.sh is the paid tier: hardened drop in components and container images held to zero exploitable critical or high CVEs, SLSA L3 or higher, Sigstore signed, with evidence pages; an artifact loses its badge if it regresses. We make no claim that Gold held a rebuilt LiteLLM before or during this incident.
The habit these support is the one this incident rewards: pull components and images from a curated, verified source, pin them, and rebuild fast when the safe version changes.
Safeguard's desktop and CLI apps are at https://safeguard.sh/download, and CLI docs are at https://docs.safeguard.sh/docs/cli .
Common mistakes
- Running
pipto check the version. Starting Python in an affected environment re-runs the payload. Usefindand file listings first. - Searching gateway logs by printing matches.
grepwithout-cor-lcopies every token into your terminal scrollback, your shell history if piped, and whatever ticket you paste into. Count, then revoke. - Revoking only the three LiteLLM tokens in your head. If your gateway logged content, any token that ever passed through an agent session is in scope, not only GitHub tokens.
- Purging logs before revoking. Revoke first, then purge. Purging first leaves a live token and destroys the evidence of when it was logged.
- Treating "two approvals" as "two people." If both approvers' tokens can be stolen the same way, the second approval adds nothing.
- Trusting deleted run history. If release runs are missing, check
workflows.delete_workflow_runin the audit log before you conclude nothing ran. - Assuming a private gateway stays private. LiteLLM's was described as internal. Test reachability from outside, and check for accounts nobody created.
- Blaming one tool and stopping there. Whatever the "specific situations" turn out to be, any agent that runs a shell can see what the shell sees. The controls above work regardless of which agent you use.
FAQ
Did Claude Code leak GitHub tokens?
LiteLLM says it observed "specific situations" in which Claude Code put a user's GitHub CLI OAuth token into a response body, and that such tokens may appear in LiteLLM logs where response bodies are logged. LiteLLM has not described those situations publicly, and we found no statement from Anthropic. Treat it as LiteLLM's finding, not a confirmed Claude Code vulnerability.
Is this a LiteLLM vulnerability?
Two separate things are in play. The malicious 1.107.0 release was a release pipeline compromise. Separately, LiteLLM suspects its internal deployment was entered through "a recently patched vulnerability," not confirmed, and says an embargoed enterprise fix for a vulnerability is due for disclosure on 27 October. It has not said whether those are the same.
I use LiteLLM as a gateway but never installed 1.107.0. Am I affected?
Not by the malicious release. But the post mortem lesson applies to you directly: if your gateway logs message content, search it for tokens, restrict who can read it, and keep it off the internet.
Does this only matter for LiteLLM?
No. Any gateway or proxy that stores request and response bodies holds whatever your tools sent and received. Anthropic's documentation lists audit logging of every model request as one of the things a gateway provides; that is exactly the data to protect.
What GitHub token types should I look for in logs?
GitHub documents six prefixes: ghp_, github_pat_, gho_, ghu_, ghs_ and ghr_. LiteLLM calls the leaked credentials GitHub CLI OAuth tokens, and GitHub documents OAuth access tokens as starting with gho_. Also search for your cloud and model provider key formats.
Branch protection was on. How was it bypassed?
LiteLLM says the change was rejected at 10:08:29 and a privileged credential, one belonging to an organization owner, overrode the protection five seconds later. GitHub's documentation says admins bypass branch protection by default unless "Do not allow bypassing the above settings" is enabled.
LiteLLM had trusted publishing and signed images. Why did that not stop it?
Because the attacker drove the real release workflow. Signing and trusted publishing prove which workflow built an artifact; they do not prove the workflow was building from the right source. The fix is to check the source repository identity inside the release job, and for consumers to check provenance against the expected repository.
How fast did the attacker use stolen cloud credentials?
Researchers cited by The Hacker News saw validation of stolen AWS instance role credentials between 16:20 and 17:01 UTC on 21 October, about four and a half hours after exfiltration, from 168.100.8[.]57 and 165.227.201[.]8, followed by Bedrock model listing calls.
What should I rotate if my gateway logged content?
Any credential that could have appeared in agent sessions routed through it: GitHub tokens, cloud keys, model provider keys, database URLs and anything in .env files those agents read. Revoke at the issuer first, then purge the logs.
Fix it without waiting for upstream
In this incident the fix already existed, litellm==1.106.2, so the risk sat in how quickly teams could find every place that pulled 1.107.0 or a moving tag and pin back.
In incidents like this one, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would have helped
These demos show general Safeguard flows, not this incident. Each intro says how the same flow applies here.
The CLI demo fixes a lodash CVE: it reads the finding and advisory, asks before each command, edits package.json, runs npm install and npm test with your approval, and pushes nothing. For this incident you would ask the same assistant to pin litellm==1.106.2 with hashes in your requirements, replace latest and main-stable image tags with digests, and rerun your tests. Because it asks before each command, you see exactly what runs, which is the point of the agent lessons above.
Click through it at your own pace, or open it full screen.
The mobile app demo is a real Android session: you send work to a computer running the CLI and approve each command on the phone with Allow once, Allow for turn or Deny. Applied here, an engineer away from the desk could approve the pin and rebuild on a gateway repository command by command, and deny anything that reaches for credentials it should not touch.
Click through it at your own pace, or open it full screen.
The Claude demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. That choice is the same least privilege idea this post argues for: give the agent only what the task needs. Here you could ask Claude which of your projects contain litellm and at what version, without handing it broader access.
Click through it at your own pace, or open it full screen.
For the wider picture of this month's incidents, see October 2026 security incidents and what would have helped, and for a general playbook, see incident response for a compromised dependency.
Quick check
Which GitHub setting would have stopped an organization owner's token from overriding branch protection?
"Do not allow bypassing the above settings" on the protected branch, which applies the rules to admins and roles with the bypass permission. Rulesets with an empty bypass list do the same job.
How do you search gateway logs for GitHub tokens without exposing them again?
Search for the documented prefixes (ghp_, github_pat_, gho_, ghu_, ghs_, ghr_) and output only counts or file names, for example with grep -c or grep -l. Revoke any token found at GitHub before purging the log entries.
Sources
- LiteLLM: Security incident, 21 October 2026
- GitHub: BerriAI/litellm issue #47826
- LiteLLM: Security update, March 2026
- LiteLLM docs: Logging
- LiteLLM docs: Claude Code client setup
- Anthropic Claude Code docs: Other LLM gateways
- Anthropic Claude Code docs: Gateway compatibility guide
- Anthropic Claude Code docs: Security
- GitHub Docs: About authentication to GitHub (token prefixes)
- GitHub Docs: About protected branches
- GitHub Docs: Deployments and environments
- GitHub Docs: Audit log events for your organization
- GitHub CLI manual: gh auth login
- The Hacker News: LiteLLM suspects GitHub token leak
- The Hacker News: LiteLLM compromised in supply chain attack
- The Register: LiteLLM supply chain attack
- Gigazine: LiteLLM backdoor
- Safeguard: LiteLLM 1.107.0 PyPI compromise
- Safeguard: Is an LLM gateway a security risk?
Last reviewed: 22 October 2026. This is a developing story; check the linked advisories for the latest status.