15:27, and the push that didn’t happen
At 15:27 on Thursday 5 November, a push to my brain repository was refused. Not by a merge conflict, not by a failing test. A secret scanner stopped it because it found a credential in the tree, and the tree was 82 commits ahead of origin with more work queued behind it.
I had a backlog to clear that afternoon. Instead the afternoon became a stop-the-line job, and my first assumption was the obvious one: a key hard-coded into a script, or a config file committed by accident. I have made that mistake before. It is the mistake every scanner is built to catch.
It wasn’t in the code. It was on line 128 of a conversation page.
What the brain actually is
The brain is a git repository of markdown pages. One page per decision, per entity, per incident, per conversation worth remembering. My agent reads it and writes to it all day. It is not a chat log I occasionally consult; it is the thing that makes the agent useful across sessions, because it can look up what we settled three weeks ago instead of asking me again.
Which means the repository is not a codebase with some notes in it. It is a record of things I have said. And things I have said include things I should not have said in a chat window.
There is a second layer under the pages. A rebuild regenerates them from the raw session database, so any page that has drifted, or been edited by hand, or been lost, can be reconstructed from the source. On 5 November that rebuild had regenerated 274 pages. That number turned out to be the whole story: the credential was not in one page by accident. It was downstream of the chat where it was first discussed, and the rebuild had faithfully carried it forward 274 times.
How a token ends up in a memory
The honest sequence is unglamorous. Weeks earlier I had set up a messaging bot, and the last step was handing the bot token to the agent so it could write it into a config file. The fastest way to do that was to paste it into the chat. Thirty seconds of work.
The agent did the right thing with it at the time and the wrong thing with it afterwards. It wrote the config. It also wrote the conversation to the session database, because that is what it is for. Later, the memory rebuild read the session database, found a conversation containing a credential, and turned it into a page. The page went into the repo. The repo went to a remote. Nothing in that chain was a bug in the ordinary sense. Every step did exactly what it was designed to do.
That is what makes this class of leak different from a leaked API key in source. A scanner pointed at code would never have seen it. A scanner pointed at the repository only saw it because the repository contains prose. And the prose was accurate, well formatted, and correctly filed.
The guard that caught it was there by accident
I want to be precise about this, because it would be easy to write a tidier story where I saw the risk coming.
I did not. The pre-push scan exists because of an earlier, unrelated mess in a different repo. It was installed to catch a specific sort of sloppiness I had already been bitten by once, and it happened to run against the brain as well. It caught this because it was thorough, not because it was aimed here. The lesson is not “I planned for this”. The lesson is that a blunt gate applied to everything catches things a clever gate aimed at the right target would have missed, because you rarely know where the right target is until afterwards.
The four hours that mattered
Recovery was four separate jobs, and doing three of them would have left the hole open.
Rotate first. The token was revoked in BotFather before anything else was touched. Rotation is the only step that makes the credential worthless to anyone who already has a copy, and everything else is tidying. The order matters: redact first and you have a false sense of safety while the token still works.
Redact at rest. The page was cleaned, and so were the source rows in the session database. This is the step people skip. If you only clean the page, the next rebuild reads the database and puts the credential back. I had just watched a rebuild do that 274 times, so cleaning the output and leaving the input alone would have been the same mistake with extra steps.
Move the guard outside the repository it protects. The scan had been living inside the thing it was guarding, which is comfortable and wrong. A guard that ships with the repo is a guard that can be removed by whoever has a clone, and a fresh clone gets a fresh copy of the guard along with everything the guard is supposed to inspect. Moving it out means one gate that runs before any repository, independent of what any clone contains. The gate belongs at the boundary, not inside the room.
Scan the repos that had no gate at all. Two repositories had never been covered. Once I knew the leak class existed, leaving those unwatched was not a defensible position. They went in as report-only, deliberately. A scanner that starts blocking pushes on day one, on repos nobody has calibrated it against, gets disabled by the end of the week. Report-only first, then talk.
Verified, not assumed
As of 8 November, three checks pass. The old token returns Unauthorized, which means the rotation took effect at the provider and not just in my notes. There are zero matches for the token anywhere in the tree. And the repository is back to 0 commits ahead of origin, with the backlog cleared rather than abandoned.
I am not going to quote any part of the token here, or describe its shape, or say which digits it started with. That is not coyness. The whole incident is about a credential surviving in text that was written to be read.
The email that had to say “do not paste it into a chat”
Here is the part I keep coming back to. To get the new token into place, I had to write an email explaining how to hand it over, and that email had to contain the sentence “do not paste it into a chat”.
Pasting it into a chat is exactly how the old one arrived there.
There is no tidy way around that. The instruction has to exist because the convenient path is the dangerous one, and the convenient path is convenient precisely because the agent is useful. You cannot remove the risk by making the agent less useful. You can only put a boundary where the useful thing stops being safe: a secret typed into a form that writes to a file, rather than into a conversation that becomes a memory.
What I actually changed
The easy fix was to add another scanner and call it hygiene. The fix I made instead was to treat memory as a place where secrets can be, which it now obviously is. Rotate anything that touches the system. Redact at ingestion and at rest, both ends, because one end regenerates the other. Gate at the boundary, outside the thing being gated. And cover the repos you have been quietly not covering.
My agent’s memory was the leak vector. Not the code, not a misconfigured service, not a stranger on the internet. The part of the system whose entire purpose is to remember what I told it.
It is still remembering. That’s the point of it. I just have a gate on the door now.