Keeping API keys out of the agent's reach
A coding agent needs an API key, or a login that does the same job, to reach its model. The key is billed to you, and anyone who holds it can spend on your account, under your name. In this lesson we take a fixture agent whose key sits in a settings file it can read and commit, and move the key out of the files it can read in two steps. Then we run a small secret scanner over the fixture and the session transcript, and we end with the rule for a key you think has leaked.
The fixture is a small program in place of your coding agent. It never calls a model. It does the one thing this lesson is about, which is to find a key at start and say where the key came from. The setting names it uses are close to Claude Code’s, and the prose says which real setting each step maps to, checked against the vendor’s documentation on the date at the end of the lesson [1]. Other coding agents have the same three places for a key under other names. Every key in the fixture is fake. You don’t need your real key for any step.
Where the fixture keeps its key today
Section titled “Where the fixture keeps its key today”The fixture is in the course repository under
site/examples/coding-with-agents/keys-and-secrets/fixture-repo/. Copy that
directory somewhere, or work in a clone of the course repository, and change
into it. The course repository ignores every .env. So the application’s
secrets file is committed as env.sample. Run cp env.sample .env once
before you start. git checkout -- . puts every tracked file back.
Inside are agent.py, the fixture agent, vault.py, the lesson’s
replacement for the command-line tool of a password manager, scan.py, the
scanner, a .env
file with two fake application secrets, a transcript.txt from an earlier
session, and .agent/settings.json, the agent’s settings file. Open that
file.
{ "env": { "AGENT_API_KEY": "sk-fake-lesson-key-0000-0000-0000-c0de" }, "permissions": { "deny": [] }}The env block sets an environment variable for every session, and here it
sets the key. Start the agent and it tells you so.
Start the agent
Section titled “Start the agent”Run this, and compare what you see with the output below.
python3 agent.pykey sk-fake-...c0de from .agent/settings.json (env.AGENT_API_KEY) deny rules: none
Output verified in CI from site/examples/coding-with-agents/keys-and-secrets/where_is_the_key.py.
That file has two problems. The agent can read it, like any file in its
working directory. Whenever the agent looks at its own configuration, the
key ends up in the session’s context. And the file is meant to be committed:
in Claude Code, the matching .claude/settings.json is the shared project
file that the whole team checks into source control, while
.claude/settings.local.json is the personal one, which Claude Code adds to
your git excludes the first time it writes it [1]. A key
in the shared file is one git push away from everyone with access to the
repository.
The examples in this lesson aren’t graded. Each one is a command you run in the fixture, with the output the course itself checks on every build. The checkpoints ask about the decisions, because deciding where a key may be is what this lesson teaches.
Before you change anything, run the scanner from the last section once, so you know the starting count. It reads dotfiles too.
Scan the fixture as it ships
Section titled “Scan the fixture as it ships”Run the scanner over the directory.
python3 scan.py.env:1 password assignment .env:2 fake token transcript.txt:4 fake key .agent/settings.json:3 fake key 4 finding(s)
Output verified in CI from site/examples/coding-with-agents/keys-and-secrets/scan_fixture.py.
Four findings, and the last one is the key you are about to move.
Step one: the key becomes an environment variable
Section titled “Step one: the key becomes an environment variable”Delete the env block from .agent/settings.json. The file then reads
{"permissions": {"deny": []}}. Now give the agent the key through your
shell instead. Where does the shell get it? From your password manager.
Most password managers have a command-line tool that asks you to unlock the
vault and prints one secret. vault.py is the lesson’s replacement for that
tool. It prints the fake key, and the lesson runs without a password manager.
The scanner never lists vault.py, because the script assembles the key from
parts at run time and the whole string is in no file.
Start the agent with the key in the environment
Section titled “Start the agent with the key in the environment”Set the variable for this one command from the vault’s output, and start the agent.
AGENT_API_KEY="$(python3 vault.py)" python3 agent.pykey sk-fake-...c0de from environment variable AGENT_API_KEY deny rules: none
Output verified in CI from site/examples/coding-with-agents/keys-and-secrets/key_from_environment.py.
The agent works the same, and no file in the repository holds the key. For
Claude Code, the variable is ANTHROPIC_API_KEY. When it is set there is no
browser login: Claude Code shows the key it found and asks you to confirm it
[2]. Create that key as a personal key in the
Claude Console and store it in the password manager the moment the console
shows it. A personal key acts as you and stops working when you leave the
organization. A shared key acts as one person for everyone who holds it, and
breaks for everyone when that person leaves [3].
Setting the variable in your shell’s startup file (.zshrc, .bashrc)
puts the key in another plain-text file, and every program you start can
read it. The line above asks the vault each time instead. That is one more
prompt per session, and it is the step that keeps the key out of any file.
Step two: a key helper, and deny rules for the files that stay
Section titled “Step two: a key helper, and deny rules for the files that stay”The second edit removes the prompt without bringing the file back. Instead
of you fetching the key and handing it over, the agent runs the vault
command itself at start. Edit .agent/settings.json to this:
{ "keyHelper": "python3 vault.py", "permissions": { "deny": ["Read(./.env)", "Edit(./.agent/settings.json)"] }}Start the agent with a key helper
Section titled “Start the agent with a key helper”Start the agent with nothing set in the shell. It runs the helper and reports the deny rules it loaded.
python3 agent.pykey sk-fake-...c0de from keyHelper command: python3 vault.py deny rules: Read(./.env), Edit(./.agent/settings.json)
Output verified in CI from site/examples/coding-with-agents/keys-and-secrets/key_from_helper.py.
In Claude Code the setting is apiKeyHelper: a shell command that returns
the key, run at start and again after five minutes by default
[2]. The settings file then holds the name of a
command, which is safe to commit, and the password manager holds the key.
The deny rules cover the secrets that stay in the repository because the
application needs them. .env is the usual home of a database password or
a mail token, and a coding agent reads it as readily as any other file.
A Read(./.env) deny rule blocks Claude Code’s file tools from opening it,
and the vendor’s documentation names one limit that matters here: the rule
covers the built-in tools and file commands the tool recognizes, such as
cat and head, and doesn’t cover a script that opens the file itself.
For enforcement that holds against every process, the same page points at
the sandbox, which a later lesson in this course covers
[4]. The Edit rule on the settings file keeps the
agent from changing its own deny list. Claude Code already treats a write
under .claude/ as a protected path that the normal modes prompt for, and
an Edit(./.claude/settings.json) deny rule is the explicit form of the
same decision [5]. The fixture agent prints the
rules and doesn’t enforce them, because enforcement is the real tool’s job.
A .gitignore entry does a different job. It keeps .env out of commits,
and it does nothing about the agent reading the file, because the agent
reads the working directory and not the repository history.
Where may the key be?
Section titled “Where may the key be?”A learner has moved a coding agent's API key out of the agent's settings file. The agent reads every file in its working directory, the shared settings file is committed to git, and a session transcript records what the agent printed.
Ask two questions of each place: can the agent read it, and can it end up in a commit or a transcript?
Scan the fixture and the transcript
Section titled “Scan the fixture and the transcript”You moved the key by hand, and a hand is what forgets a file. A secret
scanner is a program that reads every file for strings that look like a
key or a password and reports each one with a file name and a line number.
Real scanners know hundreds of vendor formats and check git history as
well. gitleaks is one, and it is free software [6]. scan.py is
the lesson’s replacement, with the fixture’s fake formats and the two patterns
every scanner starts from: a password assignment, and a long random-looking
string after a key-like prefix.
Scan the fixture after the move
Section titled “Scan the fixture after the move”Run the scanner again.
python3 scan.py.env:1 password assignment .env:2 fake token transcript.txt:4 fake key 3 finding(s)
Output verified in CI from site/examples/coding-with-agents/keys-and-secrets/scan_after_move.py.
Read the report. The settings file no longer appears, which is what you
set out to do. .env appears twice, as expected, and it is why the deny
rule exists. The third line is the one to stop at: transcript.txt holds
the key. In an earlier session someone asked the agent how it was
configured, the agent read its settings file and quoted the key in its
answer, and the transcript kept it. A transcript is a file like any other.
It may end up in a log directory, or someone may paste it into a bug report.
From this point, treat that key as exposed.
Is .gitignore enough?
Section titled “Is .gitignore enough?”A team keeps its database password in a .env file in the repository the coding agent works in. The file is listed in .gitignore. The agent has no deny rules.
A colleague says: “The .env is in .gitignore, so the agent can’t leak
it.” Is that right?
Which action does .gitignore act on, and which action are you worried about?
A key you think has leaked
Section titled “A key you think has leaked”The transcript question has one answer: revoke first, investigate second. A leaked key is used quickly, and rotation is cheap. In the Claude Console the keys page has a Disable action that is reversible and a Delete action that is permanent, and the vendor’s own advice for a key you no longer trust is to turn it off or remove it [3]. Create a new key, store it in the password manager, and start the agent again through the helper. Nothing else in your setup changes, because nothing else held the key. Then work out how the leak happened, so it happens once.
The same page suggests an expiry date when you create a key. A key that expires in thirty days limits how long a leaked copy keeps working, and it still isn’t a substitute for keeping it out of files [3].
The scanner finds the key in the transcript
Section titled “The scanner finds the key in the transcript”A learner ran a secret scanner over a coding agent's working directory and found the agent's own API key quoted in a saved session transcript. The key is still active, and the agent now gets its key through a helper command from a password manager.
The scanner reports transcript.txt:4 fake key. In your own setup that
line names a real key that is still active. What do you do first?
Which action makes every copy of the key worthless at once?
How the key reaches the agent
Section titled “How the key reaches the agent”A coding agent reads every file in its working directory, the shared settings file is committed to git, and a session transcript records what the agent printed and was told.
How does the agent get its key without the key being in a file it can read?
Which way keeps the key out of every file the agent can read or commit?
Put the response in order
Section titled “Put the response in order”A secret scanner found an active API key in a file in the coding agent's working directory.
- Revoke the key in the provider's console
- Create a new key and store it in the password manager
- Find out how the key got into the file
What stops the leaked key from working, and what can wait until it can't be used?
After the session
Section titled “After the session”A learner checks a coding agent's working directory for secrets after a session. The directory holds the repository, a .env file listed in .gitignore and the session transcripts.
Which two steps belong in the check after a session?
Which files can hold a secret, and who reads the scanner's report?
Exercise
In a copy of the fixture, do the two edits yourself: remove the env block
from .agent/settings.json, put "keyHelper": "python3 vault.py" and the
two deny rules in its place, and start the agent with python3 agent.py.
Then run python3 scan.py and write down, for each line of the report,
what you do about it. Plan on five minutes. This is the whole procedure,
and after one pass you can repeat it on your real agent’s settings file
without the page.
A good result: the agent reports the key from the helper and both deny
rules, the scanner reports three findings and none of them in the settings
file, and your notes say “deny rule, stays” for the two .env lines and
“revoke, then find out how” for the transcript. Then answer one question:
which file in your own projects plays the part of transcript.txt?
Stretch: If you have a password manager with a command-line tool, replace vault.py in the keyHelper with the real command that prints a test entry you create for this purpose, and confirm the agent starts. Then delete the test entry.
Recap
- A coding agent’s key is billed to you. Use a personal key, never a shared one, and keep it in a password manager [3].
- A key never goes in a file the agent can read and commit. The shared settings file is both. Hand the key over through an environment variable set from the password manager, or let a key helper fetch it at start [2].
- Deny the agent’s reads of the secrets that stay in the repository, such as
.env..gitignorekeeps a file out of commits and does nothing about the agent reading it [4]. - Run a secret scanner over the working directory and the transcript, and read every line of the report.
- A key that appears where it shouldn’t is revoked first and investigated second [3].
You can now
- Runs a session from setup to a reviewed diff
References
Section titled “References”- Anthropic. Settings files and precedence. Claude Code documentation. Reference.
Claude Code settings - Anthropic. Authentication. Claude Code documentation. Reference.
Claude Code authentication - Anthropic. Authentication. Claude Platform documentation. Reference.
Claude docs authentication - Anthropic. Configure permissions. Claude Code documentation. Reference.
Claude Code permissions - Anthropic. Choose a permission mode. Claude Code documentation. Reference.
Claude Code permission modes - Zachary Rice and contributors. Gitleaks. GitHub, gitleaks/gitleaks README. Reference.
Gitleaks