The OpenCode Breach: 1.4% of Console Users, Linked Provider Keys, and What to Do Today

·11 min read·Evergreen Tools Team

On October 18, 2026, OpenCode disclosed a security breach. The public statement, posted on X and reproduced in StepSecurity's analysis the same day, is short: a large-scale distillation attack impacted 1.4% of OpenCode Console users, all of them were notified by email that morning, and the company is working with Anthropic, OpenAI and Google to remediate while the incident is ongoing. That is the entire public record so far. No root cause, no statement about which data was accessed, no confirmation about whether provider API keys were exposed. What follows is what can be done with the information that exists, which turns out to be more than it looks.

Padlock icon overlaid on a dark circuit board

The disclosed scope is narrow. The remediation work is not.

What the vendor actually said, and what it did not

Four things are established. The impact is scoped to OpenCode Console users, not to everyone who has the CLI installed. The affected subset had external providers linked, and OpenCode names Anthropic, OpenAI and Google. Every affected user was emailed on the morning of October 18. And the incident is ongoing, with no root cause published.

Four things are not established. Which data was accessed. Whether linked provider credentials were exposed. What the attacker actually took. And whether the 1.4% figure will hold as the investigation continues. StepSecurity is explicit about this gap rather than filling it with inference, which is the correct behaviour for third-party analysis and worth copying in your own internal comms.

The practical reading: do not wait for a root cause to start work that is useful regardless of it. If a developer linked provider credentials to an agent console and those credentials were not rotated, then whether the breach touched them or not, the exposure exists now and will exist after the postmortem.

#!/usr/bin/env bash
# Inventory first. You cannot respond to a breach on machines you cannot list.
# Checks the binary, the config directory, and the MCP server config on each host.

set -u
echo "host: $(hostname)"

# macOS / Linux
for p in   "$HOME/.local/share/opencode"   "$HOME/.config/opencode"   "$HOME/.opencode"
do
  [ -e "$p" ] && echo "found dir: $p"
done

command -v opencode >/dev/null 2>&1 && echo "binary: $(command -v opencode)"

# Windows (run in Git Bash or WSL with the user profile mounted)
# %USERPROFILE%\.local\share\opencode

# version matters for scoping: pin it in your inventory
opencode --version 2>/dev/null || true

Find it on your machines before you argue about scope

Most security teams cannot answer a simple question: which developers run which AI coding agents? That is the first gap to close, and it is a documentation problem more than a tooling problem. An agent installed for a weekend experiment and forgotten is still an agent with credentials on disk.

OpenCode keeps state in a predictable place. On macOS and Linux the relevant directories sit under the user's local share and config paths, and on Windows under the user profile's equivalent. The CLI binary is discoverable through PATH. Version matters because scoping by version is the only way to tell who was plausibly affected when the vendor eventually publishes details.

If you want fleet visibility, StepSecurity's Dev Machine Guard is open source and runs as a one-time scan on a single machine, and it detects the CLI binary, the config directory and the MCP server configuration across macOS, Windows and Linux. It reports what is installed. It cannot tell you who has a Console account, which is why the inventory list has to be followed by asking people.

# Check which providers are linked, WITHOUT printing the secrets themselves.
# The vendor's statement says affected users are the ones with external providers
# linked, so this file is the first thing to look at.

import json, os, pathlib

paths = [
    pathlib.Path.home() / ".local/share/opencode/auth.json",
    pathlib.Path.home() / ".config/opencode/auth.json",
]

for p in paths:
    if not p.exists():
        continue
    data = json.loads(p.read_text())
    print(f"file: {p}  providers: {len(data)}")
    for provider, cred in data.items():
        kind = "oauth" if isinstance(cred, dict) and cred.get("type") == "oauth" else "api-key"
        # print the shape, never the value
        keys = sorted(cred.keys()) if isinstance(cred, dict) else ["<string>"]
        print(f"  - {provider:14} type={kind:8} fields={keys}")

# Never paste the file into a ticket, a chat, or a screenshot. Copy the provider
# names and rotate; the values themselves are the thing you are trying to retire.

The file that decides everything: auth.json

OpenCode stores provider credentials in a local auth file, and StepSecurity names the paths: the local share directory on macOS and Linux, the user profile equivalent on Windows. Every provider listed in that file is a credential worth rotating. That sentence is the whole remediation, and it is also where people make their first mistake by pasting the file into a ticket.

Print the shape, not the values. Provider names, credential type, field names, and the count. That is enough to build a rotation list and to check the claim that only users with linked external providers were in scope. The values themselves should never leave the machine, because the point of the exercise is to make them worthless.

There is a second credential store people forget: the MCP server configuration. MCP servers frequently carry their own tokens for whatever they connect to, so a review of OpenCode that stops at auth.json is incomplete. Diff the MCP config against your last known-good copy and look for anything added recently.

#!/usr/bin/env bash
# Rotation order matters. Rotate the key that can spend money on inference first,
# then the one with the widest data reach. Verify the new key works before you
# revoke the old one, and keep an overlap window you can reason about.

set -euo pipefail

rotate() {
  local provider="$1"
  echo "== $provider =="
  echo "1. create new key in the provider console (do not reuse the old name)"
  echo "2. write it to the secret store, not to a dotfile:"
  echo "     your-secret-tool set ${provider}_API_KEY"
  echo "3. smoke test with the NEW key:"
  echo "     curl -sS -o /dev/null -w '%{http_code}' "$2" -H "Authorization: Bearer \$NEW_KEY""
  echo "4. revoke the OLD key in the console; confirm it now returns 401"
  echo "5. record rotation time in the incident timeline"
  echo
}

rotate anthropic "https://api.anthropic.com/v1/models"
rotate openai    "https://api.openai.com/v1/models"
rotate google    "https://generativelanguage.googleapis.com/v1beta/models"

# Step 4 is the one people skip. A key you rotated but never revoked is still a
# valid credential sitting in an attacker's list.
Code on a dark screen, close up

auth.json is a local file listing every provider you linked. Treat it as a credential store.

Rotate in the right order, then read the bill

Rotation order should follow blast radius. The key that can spend money on inference goes first, because abuse shows up as a bill before it shows up as an alert. Then the key with the widest data reach. Create the replacement, write it to a secret store rather than a dotfile, smoke test it, and only then revoke the old one, confirming that reuse now returns 401.

That last step is the one teams skip under time pressure. A key you rotated but never revoked remains a valid credential in somebody else's list, and the incident closure meeting will not catch it because the new key works. Verify revocation explicitly, one key at a time, and record the timestamps.

Then read the usage history, because rotation stops future abuse and says nothing about past abuse. Look for shape rather than volume: a burst of short requests, an unusual model, an unusual region, activity at hours the developer does not work. Provider export formats differ, so normalise before you judge. This is also the moment to check whether the agent's own console account shows sessions the developer does not recognise.

# Usage anomaly review. Rotation stops new abuse; usage history tells you whether
# abuse already happened. Look for shape, not volume: a burst of short requests
# with unusual model or region, at hours the developer does not work.

import json
from datetime import datetime
from collections import Counter

rows = [json.loads(l) for l in open("provider-usage.jsonl")]

def hour(dt): return datetime.fromisoformat(dt).hour

peaks = Counter(hour(r["ts"]) for r in rows)
models = Counter(r["model"] for r in rows)
regions = Counter(r.get("region", "unknown") for r in rows)

print("top hours:", peaks.most_common(3))
print("top models:", models.most_common(3))
print("regions:", regions.most_common(3))

# Flag anything a human would recognise as odd:
for r in rows:
    odd = (
        r.get("region") not in {"your-usual-region"}
        or r["model"] not in {"models-you-actually-use"}
        or r.get("prompt_tokens", 0) > 100_000
    )
    if odd:
        print("REVIEW", r["ts"], r["model"], r.get("region"), r.get("prompt_tokens"))

# Provider export formats differ. Normalise first, then judge. And do this before
# you conclude "we rotated, so we are fine".

What to change so the next disclosure costs you less

The durable fix is not better incident response, it is a shorter list of long-lived credentials. Prefer short-lived tokens where the provider supports them. Prefer a gateway-mediated key that can be scoped, rate-limited and revoked centrally over a raw provider key pasted into a tool's config. And prefer an inventory you maintain continuously over one you assemble during an incident.

The second durable fix is an honest definition of scope. "No email means not impacted" is the vendor's claim, and it may be correct, but it is a claim made during an ongoing incident with no published root cause. Your own control should not depend on the vendor's email delivery working.

The third is writing down what you do not know. A one-page incident note with an explicit unknowns section is worth more than a confident summary, because the unknowns are what tell you which monitoring to add. The checklist below is deliberately boring, and boring is what makes it survive contact with a Friday afternoon.

#!/usr/bin/env bash
# Incident closure checklist. Cheap to run, and it is the difference between
# "we handled it" and "we hope we handled it".

set -euo pipefail
cat <<'EOF'
[ ] vendor statement archived (timestamp + URL + screenshot)
[ ] scope confirmed: Console users vs CLI-only installs (they are different)
[ ] every developer asked: did you receive the vendor email?
[ ] auth.json provider list captured (values never copied)
[ ] Anthropic / OpenAI / Google keys used with OpenCode rotated
[ ] old keys confirmed revoked (401 on reuse)
[ ] provider usage reviewed for the incident window, anomalies triaged
[ ] MCP server configs reviewed for credentials of their own
[ ] no-plaintext-key policy applied to the affected machines
[ ] timeline written, owner named, follow-up date set
EOF

# Two things to resist: treating "no email" as proof of safety, and treating
# "keys rotated" as the end of the work.
Illuminated server room corridor

Usage history answers the question rotation cannot: did anyone already spend your budget?

📌 Frequently Asked Questions

How many users were affected?

OpenCode's public statement says the breach impacted 1.4% of OpenCode Console users. The vendor also says the incident is ongoing and that it cannot share more details yet, so the figure should be treated as current rather than final.

Does it affect everyone who installed the CLI?

According to the statement, the impact is scoped to OpenCode Console users rather than everyone who has the CLI installed, and within that group the affected subset had external providers linked. The providers named are Anthropic, OpenAI and Google.

Were API keys exposed?

That has not been established publicly. StepSecurity notes that it has not seen details about what data was accessed or whether provider API keys were exposed. The prudent assumption is that any key linked to an agent console should be rotated, because rotation is cheap and the uncertainty is not.

If I did not get an email, am I safe?

The vendor says it emailed every affected user on October 18 and that no email means you were not impacted. That is the vendor's assertion during an ongoing incident with no published root cause. Treat it as one input, verify it against your own inventory and usage logs, and rotate anything you cannot confidently account for.

Where does OpenCode store provider credentials?

In a local auth file: the local share directory under the user's home on macOS and Linux, and the equivalent user profile path on Windows, as documented in StepSecurity's guidance. List the providers in that file, rotate each one, and never copy the file's values into a ticket.