How to Give an AI Coding Agent a Secret Without Putting It in the Prompt

An agent needs a Stripe key or a database password to run its command. Paste it in the chat and it lands in the transcript on disk, at the model provider, and sometimes in a commit. The rule we ended up with after three of our own mistakes: a secret reaches an agent through an environment variable and nothing else.

The situation is always the same. An agent is halfway through a task, it needs to call your Stripe test API, run a migration against the staging database, or push to a private registry. It asks for the key. You have the key in your password manager, the agent is waiting, and the fastest thing in the world is to paste it in the chat.

That paste is the moment the key leaves your control. Not because the model is malicious, but because a prompt is a stored, forwarded object: it goes into a transcript file on your disk, it goes to the model provider, and it gets copied by every retry and every subagent that inherits the conversation.

We build AgentsRoom, a desktop app that runs coding agents, so we have had to solve this for ourselves. We got it wrong three times in September 2026. This is the rule we ended up with, the three mistakes that produced it, and how to apply it whether or not you use our app.

The short answer

  • A secret reaches an agent through an environment variable, and through nothing else. Not a message, not a ticket, not a skill, not a command-line flag.
  • The agent uses $NAME, never the value. The shell expands it when the command runs.
  • Store the value once, outside the repo, encrypted by your OS keychain, and reference it by name everywhere else.
  • A secret that has been through a conversation is burned. Rotate it, then clean up.
  • Redaction is a safety net, not a plan. It catches what looks like a key, not a database password that reads like a word.

Why the paste is worse than it looks

Three things happen at once, and only the first one is obvious.

It goes to the model provider. That is the part everyone sees. Your key is now in a request body on someone else's infrastructure, inside a conversation you cannot edit.

It goes on your disk, in plain text. Claude Code, Codex, Antigravity CLI and the rest keep a transcript of the conversation so you can resume it. That file sits in your home folder, unencrypted, for as long as the CLI keeps its history. Search your transcripts for the string sk- once and see what comes back.

It spreads sideways. The conversation is the agent's memory, so anything that copies the conversation copies the key: a resumed session, a forked agent, a subagent spawned with the parent's context, a summary written into a project file. We have seen a key travel from a chat message into a ticket description into a second agent's first message, with nobody doing anything wrong.

And the usual safety net does not hold. Output redaction works by pattern: sk-…, glpat-…, a JWT shape. It cannot know that thursday-harbour-71 is your staging database password. In our own app we redact matched values from terminal output and we still treat that as a net under the trapeze, not as the trapeze.

The one-door rule

Here is the rule, in one line: a credential reaches an agent through an environment variable of the process the agent runs in, and through no other channel.

Everything else is literal text. A message you type, a ticket description, a skill file, a system prompt, a command-line flag: those are all read by a human or a model, so a secret written there is a secret published.

The command line deserves its own sentence, because it feels safer than a chat message and is not. A value in --api-key=… is visible to every process on the machine through ps, it lands in your shell history, it is logged by anything that records the command that was launched, and it shows up in diagnostic tools that print the process line back to you. An environment variable of one process is readable by that process and by root. Smaller surface, same convenience.

The agent, meanwhile, never needs the value. It needs to write this:

curl -H "Authorization: Bearer $STRIPE_SECRET_KEY" https://api.stripe.com/v1/charges

The shell expands $STRIPE_SECRET_KEY at execution. The agent wrote eleven characters and a dollar sign. Its transcript holds eleven characters and a dollar sign.

How it works in our app

The mechanism matters less than the shape, but the shape is easier to copy if you can see one implementation.

One vault, one file. AgentsRoom keeps secrets in a single file in your home folder, mode 0600, encrypted through the operating system keychain, re-affirmed on every write. Nothing is synced to a server, and nothing crosses to another computer: a secret belongs to the machine that holds it. If the keychain is unavailable, saving fails rather than falling back to plain text.

You save a value once. Commands & connections > Secrets, in any project. A name (letters, digits, dot, dash, underscore, 64 characters, so it is usable as an environment variable name) and a value, typed into a password field. The row that appears shows the name and its reference, {{secret:STRIPE_SECRET_KEY}}, and a button that copies the reference. There is no reveal button and no copy-the-value button, because there is no code path that returns a value to the interface.

You reference it where the environment is built. Edit agent > Advanced > Environment variables, or the project settings, or the provider settings, or a dev command line:

STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}}

At launch, the main process substitutes the value into the environment of the terminal it is about to create. The renderer never sees it, the agent never sees it, the phone never sees it. The Secret Manager page walks through the screens.

The agent discovers names, not values. An agent connected to our MCP tools can call secrets_list and get back the list of names on that machine. That is enough for it to be useful: it can say "you have a STRIPE_SECRET_KEY saved, add STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}} to this agent's environment and I will use $STRIPE_SECRET_KEY". It cannot read the value, even while running a command that uses it.

Three mistakes we shipped

The rule above sounds obvious written down. It is the scar tissue of three bugs, all in September 2026, all reported by users.

A reference quoted in a ticket came out resolved

September 19. Robert documented a mapping in a ticket description, the perfectly reasonable STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}}, so that whoever picked the ticket up would know what to set. Reading the ticket through the API returned the reference intact. But the agent launched from that ticket received the value in its first message.

The cause was a plumbing detail with a real consequence. A ticket brief becomes the agent's first prompt, which travelled inside an environment variable of the launch command. Our resolver substituted every environment value, so it substituted the prompt too. The key went to disk and to the model provider. Rotation, no way around it.

The fix was to make the agent's launch command literal: when the environment carries an agent id, the variable that holds the first prompt is left untouched. A dev command using the same variable has no agent id and resolves as before. We accepted a side effect on purpose: a reference typed into an agent's CLI options is not resolved either, which is correct, since a command line ends up in ps and in logs anyway.

The lesson: the door has to be narrow on purpose. "Substitute everywhere it could be useful" is how a secret ends up somewhere it should never be.

A reference pasted into a message, which never resolves

September 22. Cs040618 reported the opposite problem: "I paste the reference into the conversation and the agent cannot understand what this key is."

Not a bug. A message is literal text by design, which is exactly the fix from three days earlier. But nothing in the app said so at the moment of the gesture, and the agent, receiving {{secret:…}} in a message, had no idea what to answer.

Three fixes, all about telling people where the door is: a hint that fires when you paste a reference into the composer and points at the environment variables, help text above the secrets list that names the three fields where a reference is substituted and says "never in a message", and a line in the agent's own instructions telling it what to answer when a reference arrives in a message.

"Settings > Secrets" did not exist

September 28. Mohammad's agent filed a report: the user says he added OTPIQ_API_KEY in Settings > Secrets, saved it several times over thirty minutes, and secrets_list still returns the same three old secrets.

The save path was fine. The secret had never reached the vault, because there is no Secrets page in the settings. Six help strings in the desktop app, a page on this website and the agent's own instructions all sent people to a screen that did not exist. What does exist in the settings is the environment variables block, so that is where he typed the token, in braces, which is not a reference: never substituted, sent to the agent as is, and synced with the project, so the token left the machine.

We fixed the strings to name the real path, made the agent-facing tools say where a secret is added, and added a check on the environment block that flags a raw-looking credential, a name in braces that is not a reference, and a reference to a secret that does not exist on this machine.

The lesson here is not about secrets at all: a wrong path in a help string is a security bug. It moves the user to the nearest field that looks right.

If you do not use AgentsRoom

The rule travels. Pick whichever of these fits your setup; they all end at the same place, an environment variable in the process the agent runs in.

  • A password manager CLI. op run -- claude with 1Password, or the equivalent in Bitwarden and Doppler: the tool reads references from a template, injects real values into the child process environment, and nothing is written to disk.
  • direnv with a gitignored .envrc. Simple, local, per directory. Add .envrc and .env* to .gitignore before the first value goes in, not after.
  • A .env file the agent never reads. Keep it out of the repo and load it in the shell that launches the agent. If the file is inside the repository, the agent will eventually open it, quote it in a message, and put you back where you started.
  • Cloud credentials: use short-lived ones. GitHub OIDC in CI instead of long-lived AWS keys, and a workload identity instead of a service account key file.

Whatever you pick, add the rule to the agent's instructions, so it stops asking. One paragraph in CLAUDE.md or AGENTS.md does it: never ask for a credential value, never write one into a file, use $NAME and tell the user which variable to set. We publish a small skill that says exactly that, and it is five lines long.

If a key has already gone through a conversation

In order, and the first step is not optional.

  1. Rotate it. You cannot unsend a prompt. Assume the value is compromised and issue a new one.
  2. Find the transcripts. The CLIs keep conversation files in your home folder. Search them for the old value and delete or truncate what matches.
  3. Check the places that copy text. Clipboard manager history, the ticket or issue where the agent was briefed, any summary the agent wrote into the repository, and git log -p if the agent committed while holding it.
  4. Then set up the door. New value behind an environment variable, reference by name, and the agent's instructions updated so the next one does not ask.

Step 4 last, because a clean mechanism around a leaked key is just a tidier leak.

Questions people actually ask

Why should an API key never go in a prompt to an AI agent?

Because a prompt is stored and forwarded. Claude Code, Codex and the other CLIs write the conversation to a transcript file on your disk, and the message itself goes to the model provider. From there the key is also in your clipboard history, in any log that echoes the message, and in the retries and subagents that copy the conversation. Pattern-based redaction catches things shaped like sk- or glpat-, not a database password that looks like an ordinary word.

How does an AI coding agent use a secret it cannot read?

Through an environment variable. You put the value in an environment variable of the process the agent runs in, and the agent writes $STRIPE_SECRET_KEY in the command instead of the value. The shell expands it when the command runs. The agent never sees the characters, so they never enter its conversation or its transcript.

Can I pass a secret as a command-line argument instead?

You can, and you should not. A command line is visible to every process on the machine through ps, it goes into the shell history, into process logs, and into whatever diagnostic tool reads the command that was launched. An environment variable of a single process is readable by that process and by root, which is a much smaller surface.

Where does AgentsRoom store the secrets agents use?

In one file in your home folder, mode 0600, encrypted through your operating system keychain, on that machine only. Nothing is synced to a server, and there is no button to display a value again. The app shows names, the agent sees names, and the value is only decrypted in the main process at the moment a terminal or a command starts.

What happens if I paste a secret reference into a message to the agent?

Nothing is substituted, on purpose. A message, a ticket description, a skill and a system prompt are literal text, so a reference written there arrives at the agent as text. Only the value of an environment variable is substituted. We learned that the hard way: a reference quoted in a ticket description used to come out resolved in the first message of the agent launched from it.

A key already went through a conversation. What do I do?

Rotate it first, before anything else, because you cannot unsend it: it is on the provider's side and in the transcript file on your disk. Then delete or truncate the transcripts that contain it, check your clipboard manager, and grep the repository in case the agent wrote it into a file. Only then put the new value behind an environment variable.

The short version

An agent does not need to read your key. It needs a process whose environment already holds it.

Save the value once, outside the repository, encrypted by the OS. Reference it by name where the environment is built. Let the agent write $NAME. Keep every other channel literal, including the ones where substitution would have been convenient, because the convenient channels are exactly the ones that get stored and forwarded.

And when you get it wrong, which you will, rotate first and tidy second.

Download AgentsRoom

Run all your AI agents, on all your projects, from a single window.

FreeDownload AgentsRoom

Companion app: monitor your agents on the go

Bring your own: Claude, Codex, Antigravity CLI, or other AI provider.

Get the extension
Chrome Web Store

Push bugs and requests straight to your public backlog.

Multiple projects
Multi-provider
Multiple agents
Live status
File diff & commit
Mobile companion
Live preview
Agent teams
Browser automation
Backlog-driven dev
Prompt Library
Skills Library
View all features

Keep reading