Secrets in Chat Transcripts: The API Key the Agent Echoed Into the Repo
Elena Morales
September 30, 2026
The key was in a Markdown file called docs/carrier-integration.md, inside a curl example, under a heading that said “Quick test”. It was the live production key for a parcel carrier’s rating and label API, and it had been sitting in a repository shared with an outside design agency for eleven days.
I was brought in to review the integration before launch, which is the kind of work I do: read how a small company connects to someone else’s platform and point out where the boundaries are thinner than they think. I expected the usual findings about webhook verification and over-broad scopes. I did not expect the first finding to be the key itself, in plain text, in documentation.
Nobody had committed a .env file. The .gitignore was correct. The secret scanner in CI had run on every push and found nothing. The developer who set up the integration was careful and slightly mortified. When we traced it back, the key’s route into the repository went through a place none of their controls looked at: the chat with the coding agent.
How it got there
The sequence was ordinary. On the first day of the integration, the developer wanted to check that the carrier’s sandbox and production endpoints behaved the same. He pasted the production key into the agent chat with a message like “here’s the prod key, can you hit the rates endpoint and compare with sandbox?” The agent did exactly that. It ran a curl command in the terminal with the key inline, showed the response, and noted the differences.
That was on a Monday. On Thursday, in the same long-running session, he asked the agent to write documentation for the integration so the rest of the team could test it. The agent wrote a clear, useful document. For the quick-test section, it reached back into its context for a working example of calling the rates endpoint, found the curl command it had run on Monday, and reproduced it. With the real key.
From the agent’s point of view, it had a verified, working command, and working examples make good documentation. It had no reason to treat one string in its context as different from any other. The developer skimmed the document, saw the structure was right, and committed it with the rest of the change.
The scanner missed it because the carrier’s keys are a plain 32-character hex string with no recognisable prefix. Scanners are very good at keys that announce themselves, like the ones starting with sk_live_ or AKIA. They are much worse at a random-looking string in a Markdown file, and the generic high-entropy check that might have caught it had been switched off months earlier because it flagged every hash in the lockfile.

Where a pasted secret actually goes
The documentation file was the visible copy. When we sat down to list every place that key now existed, the repository was one entry among several.
The model provider. Every message in the conversation, including the pasted key, was sent to the model provider as part of each request for the rest of the session. That is how the agent “remembers”. Depending on the plan and settings, the provider may keep requests for a period for abuse monitoring, or not at all. Either way, the key went over the wire many times, once per turn, for four days.
The tool’s local history. Most coding agents keep conversation history on disk so you can scroll back or resume. On this developer’s machine, that history lived in the tool’s application data folder, unencrypted. Anyone or anything with access to his user account, including a backup job syncing that folder to cloud storage, had the key.
Synced or shared history. Some tools sync conversations across devices or let you share a session link with a colleague. He had not shared this one, but on another project the same team had pasted a session link into a support ticket to show a bug. Any secret in that session went with it.
The terminal. The curl command the agent ran went into the shell history file, like any command typed by hand. It also appeared in the terminal output that the agent read back, so it went to the model provider again.
Every file the agent wrote afterwards. This is the one people underestimate. Once a secret is in the context, it is available for reuse for the rest of the session, and an agent will reuse whatever makes the output work. The documentation was the copy that got committed. When we searched the working tree, we also found the key in an untracked shell script the agent had written on Tuesday to test label creation.
Pasting a key into chat feels like telling a colleague. It works more like writing it on a whiteboard in a room where several people take photos and some of them keep them.
The secrets you never pasted
After this review I started asking teams a second question: has the agent ever read a secret, even if nobody pasted one?
The answer is usually yes. An agent chasing a connection error will quite reasonably run cat .env, or print the environment, or inspect a container’s configuration, or open the config file that holds the database password. Each of those puts the value into the terminal output, the output goes into the context, and from there it follows every path above. A .gitignore keeps a file out of the repository. It does nothing to keep the file’s contents out of the conversation.
In the carrier review, the agent had also run a command that printed the full environment while debugging a separate issue with the database connection. The database password was in that transcript too. Nobody had noticed, because nobody reads the full output of every command an agent runs.
What I recommend now
Most of the controls for secrets that leave through commits, client bundles and logs apply here unchanged: a scanner in the pre-commit hook and in CI, a bundle check, rotation as the real fix. What follows is specific to the conversation itself.
Give the agent a name, not a value. The agent almost never needs to see a secret to use it. Put the key in the environment the way the app will read it, and tell the agent “the carrier key is in CARRIER_API_KEY“. It can run curl -H "Authorization: Bearer $CARRIER_API_KEY" and the shell expands the variable without the value ever appearing in the command it writes. The transcript then contains the variable name, and so will any documentation the agent copies from it.
Tell the agent not to print secrets, in writing. The project’s agent instructions now include: never print environment variable values, never cat files that hold credentials, reference variables by name in commands and documentation, and if a value is needed to debug, ask the person to check it. In my experience agents follow this well once it is stated. They print the environment because it is the fastest way to debug, not because they want the password.
Use the tool’s ignore mechanism for credential files. Most agent tools have a way to exclude files from what the agent can read or index. Put .env*, credential JSON files and local config with passwords in it. This is not a security boundary, since the agent can often still run a shell command that reads the file, but it stops the casual read that happens during an unrelated task.
Use a key that can do less. For anything interactive with an agent, use a sandbox key, or a restricted key limited to the endpoints you are testing, never the production key with full permissions. The carrier supported read-only rating keys; the team had simply used the one key they had. If the leaked key had only been able to fetch rates, this would have been a minor finding.
Add a scanner rule for your own providers’ formats. Generic scanners know common key shapes. They do not know that your carrier’s keys are 32 hex characters that usually sit next to the word “Bearer” or in a header called X-Api-Key. A custom rule takes ten minutes to write and would have caught this commit.
Treat transcripts as sensitive data. Check the tool’s retention and sync settings. Do not share session links from sessions that touched credentials. Exclude the tool’s history folder from general-purpose backups, or make sure those backups are encrypted.

When it has already happened
The team rotated the carrier key within the hour: new key issued, production environment updated, old key revoked, carrier dashboard checked for unfamiliar activity. There was none. That part was the easy part, and it is the only part that actually fixes anything.
The transcript-specific cleanup came after, and it is worth being honest about its limits.
- We removed the key from the documentation, deleted the untracked script, and rewrote the branch history before it merged. The agency had cloned the repository, so the history rewrite was housekeeping, not protection.
- The developer deleted the conversation in the tool and cleared the relevant lines from his shell history.
- We checked whether the tool’s history folder had been included in any backups. It had, in a personal cloud sync. Those copies were deleted too.
- We rotated the database password as well, because it had appeared in the same transcript.
What we could not do was reach into the model provider’s systems, or confirm that every copy was gone. That is the uncomfortable truth about a secret in a transcript: you cannot list every copy, so you cannot delete every copy. Rotation is the only step that makes the copies worthless.
The boundary nobody drew
Teams think carefully about the boundary between their code and the outside world: what goes into the repository, what goes to the browser, what goes into logs. The chat with the agent sits outside that picture. It is treated as a private scratchpad, when in practice it is a document sent to a third party on every turn, stored on disk, and used as source material for every file the agent writes afterwards.
The developer in this story did nothing unusual. He pasted a key to get a quick answer, which almost everyone has done. The fix is not to be more careful at the moment of pasting. It is to set things up so the agent never needs the value: names in the conversation, values in the environment, and keys limited enough that a leaked one cannot do much.