← Back to all posts
Published on · by Renaud Deraison

The keys were in their console

On the night of October 16, someone got into a legacy system at OpenCode and reached the OpenAI, Anthropic and Google API keys that about 1.4% of OpenCode Console users had saved in their workspaces. OpenCode called it a large-scale distillation attack, and users reported provider bills they didn't run up. In Bromure Agentic Coding, provider keys stay encrypted on your Mac, multi-provider routing runs on your Mac, and any copy of a key that leaves the VM is a stand-in no provider accepts.

Your laptop was fine. Your Anthropic key was stolen from a server you had pasted it into months ago.

On Saturday, October 17, OpenCode told some of its users that it had "identified unauthorized access to a legacy system," one it was in the middle of decommissioning. The intrusion happened overnight, from October 16 into the 17th. Its notice said that "third party provider keys (eg OpenAI, Anthropic) saved in your workspace may have been exposed." OpenCode put the share of affected OpenCode Console users at about 1.4%, limited to accounts with external providers linked, and emailed each of them. It described the incident as "related to a large scale distillation attack" and said it was working with Anthropic, OpenAI and Google. OfficeChai, Startup Fortune and others covered it the same day.

OpenCode said it could not confirm whether the keys had been used, and that the incident was ongoing. Users posting on X reported charges on keys they had only ever used with OpenCode, some in the hundreds of dollars. The advice everyone printed was the same: revoke every key you saved in the Console from each provider's dashboard, issue new ones, and update every place the old key lived.

The victims hadn't run a bad package or opened a poisoned repository. An intruder took their keys from a server they had pasted them into.

Why the keys were there

OpenCode is an open-source coding agent that talks to more than 75 model providers. The Console is its hosted side, where you can save your own provider keys in a workspace and let the service use them for you. The ai-tldr summary describes them as keys saved "for bring-your-own-key use." It's a convenience: one account, every provider, no key juggling across laptops.

It also turns a vendor's database into a key ring. Every user who linked a provider added one more live, billable credential to the same store. Months later, a system the vendor was retiring still had access to that store.

A model API key is spending authority. Whoever holds it can buy inference on your account until you revoke it, at whatever rate the provider allows. The reported motive, "distillation," means harvesting a strong model's answers at volume to train a cheaper one. That takes a lot of requests, and a stolen key makes someone else pay for them.

One copy, on your Mac

Bromure Agentic Coding runs Claude Code, Codex, Grok, Kimi and omp in Linux VMs on your Mac. You register each model provider once, in Preferences › Models: paste an API key, or click Sign in with provider to use a Claude, ChatGPT, Grok or Kimi subscription with no API key at all.

The keys go into models.enc in the app's support folder, encrypted with AES-GCM under a per-install master key. That master key lives in the macOS Data Protection Keychain, bound to the app's signing identity, with iCloud sync off. It never leaves the Mac. Bromure has no account for your keys to live in, and no hosted workspace they sync to.

Bromure gives you the console's convenience on the Mac. Every machine reads the same board, and a machine can override it with its own keys. Codex, Grok, Kimi and omp can reach Bedrock, OpenRouter, xAI and Moonshot through a translating proxy that runs on the Mac and holds the real key there. Claude Code reaches OpenRouter and z.ai directly with a stand-in.

Where the real key livesKeys saved in a hosted consoleYour laptopTeammateOther usersreal keysVendor workspace storesk-ant-… sk-proj-… AIza…every linked user's keysLegacy system, being retiredIntruder runs inference at volumeThe bill goes to the key's owner until it is revoked.Keys in Bromure Agentic CodingYour Mac · models.encAES-GCM, master key in the Data Protection KeychainLinux VM · agent environment and configANTHROPIC_API_KEY=sk-ant-api03-brm-… (stand-in)Proxy on the Mac checks where each request goesapi.anthropic.comstand-in → real keyany other host451, VM pausedNo hosted copy. The VM holds only stand-ins.
Left: keys saved in a hosted console share a server with every other user's keys, and one intrusion reaches all of them. Right: in Bromure, the real key exists in an encrypted file on your Mac and in the request the Mac sends to the provider. Everything the VM holds is a stand-in, and a stand-in posted anywhere else stops the machine.

Everything in the VM is a stand-in

The agent still needs ANTHROPIC_API_KEY or OPENAI_API_KEY to start, and it gets one. Inside the VM those variables hold stand-ins shaped like the real thing: sk-ant-api03-brm-… for Anthropic, sk-brm-… for OpenAI, xai-brm-… for xAI, sk-kimi-brm-… for Kimi. The agent's HTTPS goes through a proxy on the Mac, which swaps the stand-in for the real key only on requests to that provider's own hosts: anthropic.com, openai.com, x.ai, moonshot.ai. Matching is exact or subdomain, never substring, so api.openai.com.evil.example gets nothing.

With a subscription it's the same, minus the key. The sign-in runs in a throwaway VM or through the agent's own login, the proxy captures the OAuth tokens on the Mac, and the agent keeps a stand-in. The host refreshes the access token about five minutes before it expires. The VM never holds a refresh token.

Plenty of tools now offer to sync their settings to the vendor's cloud: a CLI that "backs up" its config, an editor that keeps your workspace in your account, a plugin with a dashboard. Inside a Bromure machine, the config those tools upload holds sk-ant-api03-brm-…. A stand-in stored on someone else's server is worthless there. No provider accepts it, and it only turns into a real key when the request passes through your Mac to the provider it was minted for.

The proxy also catches the upload itself. It scans every outbound request, headers and body, for stand-ins headed somewhere other than their own provider. A config sync that carries an Anthropic stand-in to a third-party server gets HTTP 451. Nothing is forwarded, the VM pauses, and the Mac shows This environment may have been compromised, naming the token preview, the credential, the host it belongs to and the host it was sent to. You hear about it the day a tool tries to copy your key off the machine, months before any vendor's breach notice.

The morning after, in Bromure

Say you got that email anyway, because you also used a key somewhere else. Every guide says the same three things: revoke, replace everywhere, and check usage. Each step is short in Bromure.

Replace everywhere. The real key lives in one place. Paste the new one into the provider's popover in Preferences › Models and Bromure updates every running machine about a second and a half after you stop typing: the swap map, the agents' environment and their config files. Agents whose credential changed restart in their tab with their resume flags, so the conversation carries on, and the VM never reboots. There are no .env files, CI secrets or teammate laptops to chase for the copies inside the VMs, because those copies were stand-ins.

Check usage. At AI request details, the tracing level new machines start with, the Mac logs every exchange with a model provider as an llm.request record: provider, host, model, status, and input, output and cache token counts. When a provider dashboard shows a spike at 3 a.m., you can line it up against what your machines sent. bromure-cli trace summary and bromure-cli trace hostnames give the per-machine view in a terminal. Every swap of a provider key appears in the Security Timeline as a Credential brokering row, with a masked preview and the host, in a table with a Workspace column. Anything on the provider's bill that your machines didn't send came from someone else.

For a team, Bromure Enterprise Manager rolls the same llm.request counts up per user, per workspace and per model, without uploading prompts, responses or secrets. A stolen key shows up as spend that doesn't match any install.

Or skip the key. For Claude Code, Codex, Grok and Kimi, a subscription sign-in replaces the pay-per-use key, and the tokens it produces stay on the Mac and refresh there. That leaves no long-lived API key for a third party to store.

One encrypted copy, on your Mac

Provider keys and sign-ins sit in models.enc, under a master key in the Data Protection Keychain. Bromure has no hosted workspace or account that holds them.

Multi-provider routing, locally

Every machine reads one Models board. Agents reach other providers through a proxy on the Mac that holds the real key.

Stand-ins wherever a copy lands

The VM, its config files and anything a tool syncs to the cloud hold stand-ins. A stand-in sent to the wrong host gets a 451 and pauses the machine.

Rotate once, audit per request

A new key reaches every running machine without a reboot. Each model exchange is logged with its model and token counts.

Where a key should live

Every place you paste a provider key is another place it can be stolen from. Laptops get stealers, repositories get scraped, and as this week showed, vendors retire systems that can still reach their customers' keys.

Keep the real key in one encrypted file on your Mac, and put stand-ins everywhere else. For the laptop-side version of the same problem, read The stealer packed the stand-ins and The agent leaked its own key.