YFarmX logoYFarmX

AI NewsSecurity

OpenCode breach exposed customers’ AI access keys

Some OpenCode customers reported AI usage they did not authorise. Records from an older system reveal how access keys were stored and leave a question about its shutdown.

Illustration of a server and exposed keys, labelled OpenCode key breach.

Listen to this articleListen

Podcasts

OpenCode, an AI coding tool, confirmed a breach on 17 October affecting 1.4% of users of its Console account service. Some customers reported AI usage they said was not theirs. The affected users were among those who had connected outside AI services, such as OpenAI, Anthropic or Google, using their own access keys.

An API key is a secret code that lets an app use an AI service on your behalf. If someone copies a valid key, they may be able to send their own requests and put the charges on your account. They do not necessarily need your account password. That is why a key saved in one app can become a problem at another company.

OpenCode says it is working with the three AI providers. Anthropic and Google staff have described disabling affected keys. But another part of the story sits in an older version of OpenCode’s Console, the system customers used to connect those services.

A follow-up email published by an affected customer says the listed keys “were stored in the affected legacy console system”. YFarmX examined the public code and records from its shutdown. The old code saved keys in readable form. And five days before the breach announcement, parts of that system were removed from the live service, while deleting the database itself was left as a separate manual task.

Customer-published screenshot of an OpenCode notification listing provider keys stored in the affected legacy console system.
The customer-published notification links the listed keys to the legacy Console. Its 00:30 UTC time is an earliest possible exposure bound, not proof of when an attacker first entered. Inspect the original source.

Those details raise a practical question: when an old service is shut down, what happens to the keys it once held? The records explain how the old design worked and what its retirement involved. They do not yet show how the attacker got in or which store supplied the keys.

The old Console saved readable keys

When a customer entered an AI provider’s key into the old Console, the inspected code saved it unchanged. The code that saves the key and the database definition show the submitted value going directly into a text field.

A text field alone would prove little: it can also hold encrypted information. YFarmX followed the value through the save function, checked the database definition, then examined how the app used a stored key. An earlier version of the code that sends AI requests takes that stored value and uses it as the provider’s API key. Neither inspected path scrambles the key through encryption or decrypts it before use.

GitHub's historical provider save function, with the submitted credentials passed directly into the database insert.
The save step: the same credentials value accepted above is passed into the database insert at line 32. Source: OpenCode’s historical provider code. Open the image link to inspect the original lines.
GitHub's earlier request handler showing the stored provider credential assigned to the API key at line 1022.
The use step, in an earlier revision: line 1022 assigns the stored credential to the provider’s API-key field. Source: OpenCode’s request handler. Together, the two records substantiate the historical design.

These records come from different points in the app’s history. By the later version cited for storage, requests were being forwarded to a newer service. They show how the old design handled keys, without proving exactly what was running or stored when the breach happened.

The risk is straightforward. Someone who could read those saved values would have usable keys, provided they were still valid and the provider’s restrictions allowed the requests. Encrypting the disks underneath a database would not change what an authorised database reader could see: the app still saved readable secrets.

The code also contains keys issued by OpenCode itself. Its key generator saves the generated secret unchanged, and the database definition lets the app look it up by that raw value. It stores the secret itself rather than a one-way fingerprint, known as a hash. The reviewed disclosure does not establish that these OpenCode-issued keys were taken.

Historical OpenCode code generating a secret and inserting secretKey directly into KeyTable at line 66.
A separate credential: OpenCode’s own generated key goes into the table as secretKey at line 66. This is historical storage evidence; it does not establish that these keys were stolen. Inspect the original source.

One check only needed a yes or no

Moving customers to the new Console did not immediately remove the old database from the process. A code-change proposal approved on 11 October describes the old database’s remaining job: helping decide where to send some requests and whether a customer had connected their own provider key.

The code immediately before retirement makes a revealing distinction. For requests using qualifying older OpenCode keys, it checks the old database. When checking an AI provider, it asks whether the saved credential field contains anything and returns the provider’s name. It does not retrieve the secret itself for that decision.

That decision only needed a yes or no. A service can remember that a customer connected a provider without keeping a full secret just to answer that question. The earlier code shows how full keys were saved; this later check does not tell us what every saved record still contained. A non-empty placeholder would also pass it.

Historical GitHub routing code selecting the provider name and checking whether the stored credential has a positive length.
The later routing check returns a provider name and tests whether a credential field is non-empty. Its decision does not use the key’s contents. Source: OpenCode’s historical proxy code.

The forwarding code also expects the new service to recognise connections imported from the old one. That helps explain the intended move between systems. It does not prove that every customer had two complete copies of a key.

Shutting the old system down took several steps

OpenCode’s public changes show the old Console being dismantled in stages. The migration pull request records moving non-blocked workspaces while leaving old tables behind. The subsequent retirement pull request sets out a rollout ending with manual deletion of the PlanetScale database.

GitHub migration pull request states that old tables were left in place for a follow-up.
The migration record explicitly separates moving customers from dropping old tables. Leaving tables behind does not establish their contents at breach time. Inspect the original source.

The production update on 12 October records removal of the old sign-in service, called an authentication worker, and one database password. The infrastructure change removes the connection to the old database. It does not automatically delete that database: the database existed separately from the resources this update managed.

Record Time or date What it establishes
Workspace migration 11 October Non-blocked workspaces migrated; old tables left behind
Authentication worker deletion 12 October, 07:47:55 UTC The production job removed the old worker
Database password deletion 12 October, 07:48:18 UTC The production job removed that password
Deployment completed 12 October, 07:48:30 UTC The deployment finished
Customer-published notification Earliest possible exposure: 17 October, 00:30 UTC A reported exposure bound, rather than a timestamp proving extraction

Sources: the production job and a customer-published notification. Deployment times are shown to the second. The notification is a public screenshot of a purported company email.

Timeline separating the 12 October deployment from the reported earliest possible exposure on 17 October, with database deletion unresolved.
The 17 October time is the earliest possible exposure stated in the customer-published email. It is not the email’s delivery time or a proven first intrusion. Sources: the deployment record and notification screenshot.
GitHub retirement pull request lists deploying to production and deleting the old PlanetScale database by hand as separate rollout steps.
The retirement plan’s final step is manual database deletion. A plan records an intended action; it does not prove the action was completed. Inspect the original source.

Deleting a database password, deleting the database and revoking the customer’s AI key are separate actions. The password controls access to the database. The provider key authorises requests at the AI company. Removing the former does not invalidate the latter.

Diagram showing a database password controlling database access, and a separate provider key authorising AI requests. Each credential has its own revocation action.
Two credentials, two authorities: deleting access to a database does not revoke an AI provider’s key. This illustrates the distinction, rather than a reconstructed intrusion route.

The retirement pull request’s estimate of up to 257 organisations using an old route describes traffic considered during the migration. It is not a victim count and cannot be substituted for the disclosure’s user percentage. The same change deliberately retains R2 storage described in a comment as historical Zen request and response dumps. The comment says their writer was removed in May 2026; it does not show the retained contents or establish access during the incident.

The email’s earliest possible exposure bound falls five calendar days after the deployment. It also says that other data did not appear exposed at that time and that OpenCode could not confirm unauthorised use of the listed keys. Its reference to the legacy system does not identify PlanetScale as the extraction source or establish that the database survived until 17 October.

PlanetScale’s documented full deletion procedure removes all branches, data and backups. The public record reviewed supplies no completed deletion timestamp. Its organisation audit documentation lists requested and completed database deletions as separate events, alongside password-management actions, with 15-day retention on both listed plans. Those management events could help date retirement. The documented event list contains no database-read or query events, so it does not promise a record of which credential rows were read.

One suspicious request asked for a simple reply

A request screenshot published by an affected customer adds detail about reported suspicious use. The Responses interface displays a Chinese-language instruction meaning “please reply ok”, followed by an ok response.

Visible field Displayed value
Model gpt-6.1-sol
Input and output 9 input tokens; 5 output tokens
Maximum output 50 tokens
Top P 0.98
Reasoning effort and verbosity medium; medium
Created 17 October 2026, 3:12 AM; timezone absent
Customer-published Responses log showing the short prompt, ok response, model and request properties.
A customer-published request log, rather than a raw provider export. The displayed time has no timezone. Image: the customer’s original post; open it to inspect the full-size source.

Such a small exchange is consistent with checking whether a credential works. That interpretation remains an inference. The configuration may include defaults, and the screenshot supplies no source IP, user agent or destination for stolen credentials. Its language cannot identify the operator or their nationality.

OpenCode describes the incident as a large-scale distillation attack. Distillation uses outputs from one model to help train another; applying that motive to this breach is the company’s characterisation. This single short request establishes neither training activity nor resale.

The customer’s other screenshots also require careful reading. A Claude figure is month-to-date spending, while an OpenAI usage total lacks a visible selected time window. Adding those numbers into a breach loss would turn billing context into a finding the images do not support.

A key can remain valid after you stop using an app

One customer reported alerts from three providers, then said the keys had not been used in OpenCode since February. A later clarification says they had been entered into Console to supplement a Zen subscription. This is a firsthand account of dormant credentials becoming relevant again, not proof of continuous retention in a particular database.

Containment statements address the issuer side. Anthropic’s Lydia Hallie said the company had revoked Claude API keys it believed were affected. Google staff member Evan Otero described blocking exposed Gemini keys and gave replacement and billing guidance. These are public staff statements, rather than complete incident reports.

The practical distinction for customers is between removing a saved copy and invalidating the credential itself. A key that has been copied can remain useful after an application stops displaying or storing it. Revocation at the issuer closes that credential’s authority.

One Reddit commenter says they removed keys roughly three days before reporting subsequent Anthropic use. The public comment supplies neither a deletion receipt nor a provider export. The historical Console’s removal handler deletes a row from its old table, while its sign-in code redirects migrated users towards the new Console. That old handler cannot establish what happened to copies when this customer says they removed a key. A migration-related retention explanation is consistent with the public architecture, but remains unproven.

What other services can learn from this

Services that accept customers’ AI keys take on responsibility for secrets capable of generating bills. The defensive work spans storage, access and revocation. OWASP’s secrets management guidance recommends restricted access, auditing, rotation and prompt revocation of exposed secrets. It also describes encryption with protection of the encryption keys themselves.

For a service making requests on a customer’s behalf, the provider credential must be recoverable when needed. A design can protect the stored value with a separately controlled encryption key or secret-management service. That limits some database-only exposures; an attacker controlling the authorised application may still be able to use its access.

Retiring such a service needs an equally explicit record: which data stores were deleted, which copies were retained for a stated purpose, which access credentials were removed, and which customer credentials were revoked at their issuers. The OpenCode records show why each step deserves its own evidence. The useful completion test is whether every retired credential has lost its authority, with a record showing which system revoked it and when.

Sources

  1. OpenCode’s breach disclosurex.com
  2. Historical provider credential implementationgithub.com
  3. Legacy request-routing lookup before retirementgithub.com
  4. Historical OpenCode-issued key implementationgithub.com
  5. Legacy database retirement pull requestgithub.com
  6. Production deployment jobgithub.com
  7. Customer-published request logx.com
  8. Customer-published notification and billing imagesx.com
  9. Anthropic staff statement on key revocationx.com
  10. Google staff statement on key blockingx.com
  11. PlanetScale database deletion documentationplanetscale.com
  12. PlanetScale organisation audit-log documentationplanetscale.com
  13. OWASP secrets management guidancecheatsheetseries.owasp.org

How we use AI