Blog
I pasted my OpenAI API key into a Claude conversation. Here is what I found.
First-person walk through one paste: where the plaintext landed, why deleting the chat did not undo it, and the exact moment I stopped trusting human discipline to keep keys out of LLM context windows.
By Jesús E. Viera · · 8 min read
The story you are about to read is real and embarrassing. I am sharing it because the same shape of mistake happens to almost every developer who pairs an AI agent with credentials they care about, and most never notice.
On a Tuesday afternoon in March, I was prototyping a small internal tool that would let me fine-tune a private OpenAI model on a dataset of customer-support replies. I had Claude Code open, a fresh repo, and a single goal: get the fine-tune kicked off before lunch.
Claude needed an OpenAI API key. I did the thing every careful
developer has done at least once: I typed
"Here is the key:" into chat, pasted my real
sk-… string, and asked Claude to use it. The
fine-tune started. Lunch came. By the end of the day the model
was 60% trained and I had moved on to the next problem.
Two days later I was reviewing my OpenAI dashboard for an unrelated reason and noticed that key had usage from an IP I didn't recognise. Not from Claude. Not from my laptop. From somewhere I had never been.
What follows is what I traced over the next three hours. None of it was malicious — there was no breach, no leak from Anthropic, no third-party compromise. There was just my own paste, replicated in places I had not planned for it to go.
Layer 1: the chat transcript
The obvious one. The plaintext sat in my Claude Code session
history, character-for-character, surrounded by the prompt I had
typed and Claude's responses. I had used the "delete conversation"
button two days earlier when wrapping up. That button removed it
from my UI list. It did not remove it from local storage, from
the IDE's recovery cache, or from the JSONL transcript file
Claude Code keeps under
~/.config/claude-code/projects/ for resume support.
Lesson one, and the one that surprised me most: "delete chat" is not the same as "delete the bytes that contain the chat". The cache is local to me, but I have synced laptops, Time Machine backups, and a partner who occasionally borrows the screen. The surface area of "places that string still exists" was already much larger than the chat window had implied.
Layer 2: the model's context window
Claude is stateless across new conversations, but inside the conversation where I pasted the key, every subsequent prompt I sent included that key as part of the running context. There were fourteen messages in that thread. The key was in all of them after the paste.
For each message, Anthropic's API received the full conversation so far. That is the design — the model has no memory; the client sends the history. So the key was sent to the API fourteen times after I pasted it, even though I only intended to "send" it once.
Anthropic's policies are clear that they do not train on API inputs by default and they retain only for limited debugging windows. I am not accusing them of anything. The point is different: my key left my machine fourteen extra times, and I had no UI affordance that suggested this.
Layer 3: my IDE's autocomplete corpus
The one I would never have guessed. After I pasted the key,
every sk-... prefix I typed in the same project
autocompleted to my real key. My IDE had cached it. So had my
shell history, because I had also echo-ed it once
while debugging. So had a Slack DM where I had explained the
situation to a colleague using "the key starts with sk-pr-"
— which, while not the full string, was enough information to
correlate with the IDE autocomplete corpus if anyone scraped both.
No single one of these surfaces is a "breach". Each one is a perfectly normal thing that perfectly normal tools do. Together, they made the key recoverable from at least four places I had not planned for.
What about the suspicious IP?
To close the loop on the actual incident: the unfamiliar IP was a CI runner in a personal GitHub Actions job I had stopped using two months earlier. The job had cached my OpenAI key as a repository secret. After my paste, I had Claude push a refactor to that same repo. Somewhere in the diff, an old workflow file got resurrected. The runner picked up the cached secret, ran a nightly smoke test, and quietly burned $11 of API credit before I noticed.
There was no malice in any link of this chain. There was only me, a tool, and the entirely reasonable behaviour of every component in between. The key got out because I treated it like a string, and strings flow.
What I did about it
Rotated the key. Audited every place I could find that referenced it. Wrote down every layer above so I could remember next time. Then I started building ClauLock, because what I actually wanted was for the paste to be impossible — for the moment a credential crossed into the model's view to require a different gesture, one I would feel.
The mechanic is the simplest one I could come up with: when
Claude needs a secret, it cannot ask me in chat. It has to
request the secret by name through a separate channel. ClauLock
catches the request, opens an OS popup outside the model's
context, and prompts me there. I paste once. The plaintext is
encrypted to a local vault. From then on, every reference is a
placeholder like {{OPENAI_KEY}}. The model can
use the key — it cannot see it.
The full design is in how to stop leaking API keys to your AI agent. The threat model and the comparison against existing tooling is in Vault vs ClauLock. If you want to install it now and try, the picker on the home page detects your OS and gives you the right one-liner.
I am not going to pretend this is the only way. Pinning your paste discipline tightly enough probably also works. I can tell you that I tried that for years and it failed me on a Tuesday in March. Discipline is a poor security boundary.
If you only take one thing from this post
"Delete chat" is a UI gesture. It is not a security gesture. The bytes you pasted are still somewhere. They are probably in more places than you expect. The only path that closes that surface is to never put the bytes in chat in the first place.
Whatever you use to get there — ClauLock, a different secrets manager, a homemade hook — make the paste require an action that is hard to do by accident. That single change is worth more than every retroactive cleanup script combined.