The OpenCode breach: what to do when a tool leaks your AI API keys
On 17 October 2026, OpenCode, the open-source AI coding agent, told some of its users that their AI provider API keys may have been stolen. Attackers had reached a legacy part of the OpenCode Console that the company was decommissioning. It held the OpenAI, Anthropic and Google keys that users had saved in their workspaces.
OpenCode said 1.4% of Console users were affected and that only users who got an email were involved. It put the earliest possible exposure at 00:30 UTC on 17 October and called the attack "a large-scale distillation attack": stolen keys used to query large models at scale, at the key owners' expense. According to BleepingComputer, affected users reported overnight charges from several hundred dollars up to $1,700, and some had Google Cloud projects suspended for "abusive activity consistent with hijacked resources". OpenCode said it was working with the three providers and mentioned possible reimbursement.
Whether or not you use OpenCode, the lesson is the same. Every tool you paste a provider key into becomes a place that key can leak from. This post covers what to do if you were affected, how to limit the damage next time, and the tests to run if your own product stores other people's keys.
If you got the email
- Revoke the key at the provider, not just in OpenCode. Deleting it from the tool does nothing if a copy has already been taken. Anthropic said it revoked Claude keys it believed were affected, but check yours yourself.
- Look at usage from 17 October onwards.Every provider console shows usage by day and by key. Spikes on models you don't use, or at hours you weren't working, are the sign.
- Ask the provider about the charges and keep the OpenCode email as evidence.
- Replace the key everywhere it was used: CI secrets,
.envfiles, other tools. A revoked key that's still referenced shows up as 401 errors in places you forgot about.
Make the next leak cheaper
A key will leak eventually: from a tool, a laptop, a log file or a commit. What you control is how much a leaked key can do and how fast you notice:
| Control | What it limits |
|---|---|
| One key per tool | You revoke one integration, not all of them. The usage page tells you which tool leaked |
| A separate project or workspace for third-party tools | Most providers let you scope keys to a project or workspace with its own limits |
| Spend limits on that workspace | Caps the overnight bill. The damage reported in this breach happened in hours |
| Usage alerts | An email when daily spend passes a threshold beats finding out from the invoice |
| Short-lived keys you rotate | A stolen key that already expired is worthless |
It's also worth checking that you haven't leaked keys yourself. Scan the whole git history, not just the current files, because deleting a key in a later commit doesn't remove it from history:
# Secrets in the working tree and in the whole git history
gitleaks detect --source . --log-opts="--all"
# Quick look for the common AI provider key prefixes
grep -rnE "sk-ant-[A-Za-z0-9_-]{20,}|sk-(proj-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}" \
--exclude-dir={node_modules,.git,.next} .If your product stores other people's keys
"Bring your own key" is common in AI tools, and it makes you the custodian of credentials that cost money. The OpenCode breach came from a legacy system being decommissioned, which is a reminder that old systems keep their data until someone deletes it.
- Encrypt keys at rest with a key the application database can't read on its own.
- Never return a stored key in full. Show the last four characters, when it was added and when it was last used.
- Record which of your requests used which stored key, so you can tell a user exactly what happened.
- When you retire a system, delete or migrate the secrets in it before you stop watching it.
A response for a saved key should look something like this:
{
"provider": "anthropic",
"key": "sk-ant-…a9Qf",
"addedAt": "2026-10-20T09:14:00Z",
"lastUsedAt": "2026-10-26T07:02:11Z"
}Tests that catch a leaky API
Save a fake key with a random value, then search every response for it. If the full string appears anywhere, a settings page, a profile call, an export or an error body, the test fails:
// Your API should never send a stored secret back to anyone, even its owner.
import { test } from "node:test";
import assert from "node:assert/strict";
const BASE = process.env.API_BASE;
const KEY = "sk-ant-test-" + crypto.randomUUID(); // a fake key we can search for
test("a saved provider key is never returned in full", async () => {
const headers = { authorization: `Bearer ${process.env.TEST_TOKEN}`, "content-type": "application/json" };
await fetch(`${BASE}/settings/providers/anthropic`, {
method: "PUT", headers, body: JSON.stringify({ apiKey: KEY }),
});
for (const path of ["/settings/providers", "/settings/providers/anthropic", "/me", "/export"]) {
const res = await fetch(BASE + path, { headers });
const text = await res.text();
assert.ok(!text.includes(KEY), `${path} leaked the full key`);
}
});
test("errors don't echo the key either", async () => {
const res = await fetch(`${BASE}/settings/providers/anthropic`, {
method: "PUT",
headers: { authorization: `Bearer ${process.env.TEST_TOKEN}`, "content-type": "application/json" },
body: JSON.stringify({ apiKey: KEY, model: 12345 }), // invalid on purpose
});
assert.ok(res.status >= 400);
assert.ok(!(await res.text()).includes(KEY));
});Other checks worth adding:
- Another user's token can't read, replace or use your stored key (a classic broken object level authorization bug).
- Your logs don't contain the key. Run the test above, then search the log output for the fake value.
- Deleting a key really deletes it: the next request that would have used it fails.
To practise authorization tests like these against a safe target, see the auth track and our practice endpoints.