Keep secrets out of your Claude Code transcriptsA broker setup the agent can't leak.
A Claude Code transcript is a permanent record of every file it reads and every command it runs. Blocking .env access closes one of five leak surfaces. This walks through the setup that closes all five by keeping plaintext out of the shell entirely.
-
01
Move
Out of the shell
Import .env values into an encrypted vault and delete the file. Nothing sensitive is left in the environment.
no ambient secrets -
02
Reference
Handles only
The agent works with named references like @OPENAI_API_KEY and never reads the plaintext value.
value-free -
03
Inject
At exec time
The broker resolves the handle into the child process when a command runs, then logs the access.
never in context
TL;DR· the answer, in twenty seconds
The problem: Secrets reach a Claude Code transcript through five surfaces: shell inheritance, transcript echoing, file and commit writes, MCP tool arguments, and log output. Blocking .env reads closes one.
The fix: Move values out of the shell into a vault and let the agent hold references instead. A broker injects the real value into the child process at exec time, so plaintext never enters the context window.
The result: A compromised transcript, a logged prompt, or an exported session file does not contain your credentials, because the value was never there.
Secrets reach a Claude Code transcript through five surfaces: shell inheritance, transcript echoing, file and commit writes, MCP tool arguments, and log output. Blocking .env access in settings.json closes one of them and leaves the other four open. The fix that closes all five is architectural: replace plaintext values with handles the agent can reference but never read. This guide names each surface, shows why advisory rules cannot stop transcript exposure, and walks through the setup end to end.
Why the transcript is a permanent record
Claude Code's context window holds every message, every file it reads, and every command output it runs. That context is transmitted to Anthropic's API on each turn and persisted in local conversation history. It is not volatile. Once a secret enters the transcript, rotating the credential is the only remedy.
The exposure is not hypothetical. A filed GitHub issue (#44868) documents a Cloudflare API token written into conversation history while Claude Code diagnosed a .dev.vars problem. The model ran grep -n curl .dev.vars, the matching line printed to the tool result, and the token was in the transcript before any safety check fired. The model noticed, flagged it, and apologized. The credential was already there.
That is the shape of the problem. Claude Code's safety checks run on output the model has already produced, not on the side effects of the tool calls it is about to issue. A CLAUDE.md rule is advisory. It shapes intent; it does not block execution.
The five surfaces where secrets appear
Most guidance treats this as a single .env access issue. It is not. Each pathway needs a different control.
Shell inheritance. Claude Code inherits the full environment of the shell that launched it. If your shell has .env loaded through dotenv, direnv, or an rc file, every value is available to every command the agent runs. It does not need to read the file; the values are already in the environment. Knostic documented that this loading happens without an explicit prompt.
Transcript echoing. Any tool call that reads or prints file contents echoes them into the transcript: the Read tool, grep, cat, head, sed, and the rest. When the agent runs a diagnostic against a file that holds secrets, the output lands in the transcript at once. This is the surface the GitHub issue captured.
File and commit writes. Claude Code writes commits, config files, and temporary scripts. Credentials inherited from the ambient environment can appear in those artifacts. GitGuardian's State of Secrets Sprawl 2026 found that agent-assisted commits leak secrets at roughly twice the baseline rate for public GitHub (3.2% against 1.5%).
MCP tool arguments. When secrets pass as plaintext arguments to MCP tool calls, those arguments appear in the tool-call record, which is part of the context window. The same report found 24,008 unique secrets exposed in MCP configuration files on public GitHub.
Log and debug output. Test suites and build scripts often print environment-derived values to stdout, and Claude Code captures that output as a tool result. A test that prints Using key: sk-proj-... for diagnostics writes that key into the transcript.
None of these requires deliberate carelessness. They happen during ordinary workflows, using ordinary diagnostic and build commands.
Why configuration controls are necessary but not sufficient
The standard remediation for claude code secrets exposure starts in settings.json. A deny rule on .env reads blocks direct file access, and Anthropic's best-practices guide recommends it:
{
"permissions": {
"deny": ["Read(./.env*)", "Read(./.dev.vars*)"]
}
}
It is a good control, and it addresses transcript echoing through file reads. It does nothing about shell inheritance, because the agent still inherits the environment and any spawned command can echo an inherited variable. It does nothing about file writes, MCP arguments, or log output.
The underlying issue is that the shell environment is a pool of unscoped, unaudited credentials. Any process in that environment has access to all of it. No .claudeignore entry and no CLAUDE.md rule changes that. Blocking file access is not the same as blocking value exposure.
How a broker closes all five surfaces
hasp is a local secret broker for coding-agent workflows. The mechanism: secrets live in an encrypted local vault, the agent never reads them, and when a command needs a credential the value is injected into the child process at exec time. The agent sees a reference, not a value. As the mental model puts it, an agent working this way sees a smaller world than a shell with .env loaded, and that smaller world is the point.
Four gates pass before a value is delivered:
- Vault. Does this machine hold the named secret?
- Project binding. Is this repository allowed to request it?
- Grant. Does this session have an active, scoped permission for the request?
- Delivery. Inject the value into the child process tree, then write the audit entry.
The value never enters the context window. The agent works with named references like @OPENAI_API_KEY and two execution tools: hasp_run, which runs a command with references resolved to environment variables in the child process, and hasp_inject, which materializes credentials as files outside the repository. For output that might contain echoed values, the redaction layer masks eleven encoding formats in real time before the agent receives it, with marker tokens that keep tracebacks pasteable. Grants carry a hard 24-hour ceiling that no policy lifts, so one compromised session has a bounded blast radius. The agent mental model covers what the agent sees and does not see.
Setting up the broker with Claude Code
Prerequisites: a packaged release or a source build, Homebrew for the install path, a Claude Code install at version 2.x or later, and the repository you want to secure.
Step 1: Install the broker
brew install gethasp/tap/hasp
hasp version
Verify the version string returns before moving on.
Step 2: Initialize the vault
export HASP_MASTER_PASSWORD='your-master-password'
hasp init
This creates the local encrypted vault under HASP_HOME (default ~/.hasp). The password is never stored; losing it means losing the vault contents.
Step 3: Import existing secrets
hasp import .env
hasp secret list
Once the import is verified, the secrets are in the vault and not in your shell. Delete the .env file from the working directory.
Step 4: Bootstrap the repository
hasp bootstrap --profile claude-code --project-root /path/to/your/repo
This creates the project binding, the link between this repository and the vault secrets it is allowed to request, and writes the project configuration.
Step 5: Declare a value-free manifest
A value-free manifest is committed to the repository. It declares what your workflows need and how to deliver it, with no values inside:
{
"version": "v1",
"project": { "name": "your-project", "description": "Local development targets." },
"references": [
{ "alias": "secret_01", "item": "OPENAI_API_KEY" }
],
"targets": [
{
"name": "server.dev",
"command": ["npm", "run", "dev"],
"root": ".",
"delivery": [
{ "as": "env", "name": "OPENAI_API_KEY", "ref": "secret_01" }
]
}
]
}
Commit it. Teammates and CI read it to learn what the project needs without any credential exposure.
Step 6: Register the MCP server
hasp agent mcp claude-code
This prints the MCP server block to add to your Claude Code settings.json under mcpServers. After a restart, the agent has the MCP surface: hasp_list, hasp_targets, hasp_target_explain, hasp_run, hasp_inject, and hasp_redact. The Claude Code agent profile has the full configuration reference.
Step 7: Verify the broker path
Ask the agent to list available references:
Use hasp_list to show what secrets are available for this project.
It returns reference aliases and target names, no values. Then run a brokered command:
Use hasp_run to execute the server.dev target.
The command runs, the value is injected into the child process, and the transcript holds the reference name, the exit code, and the output. Not the credential.
What the agent sees: an illustrative comparison
This is constructed to show the mechanism, not a captured live session. With a bare .env in the shell, when the agent runs grep -n OPENAI .env during diagnostics, the tool result reads:
.env:3:OPENAI_API_KEY=sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdef
That line is now in the context window. It is transmitted to the API, it persists in history, and a later compaction may preserve it. With brokering, the agent calls hasp_list and the tool result reads:
{
"refs": ["secret_01"],
"aliases": { "secret_01": "@OPENAI_API_KEY" },
"targets": ["server.dev"]
}
The agent calls hasp_run on server.dev, the command executes, and the value is injected into the child process at exec time. The tool result holds the output and exit code. No key appears anywhere in the transcript. The difference is architectural: one path puts the value in the context window at the moment of use, the other never puts it there.
What this means for your stack
Start with the cheap control today: add the settings.json deny rule above so the agent cannot read .env files directly. Then be clear-eyed that it closes one surface of five. The deny rule is a fence around the file; the values are still in the shell, where any command the agent runs can echo, write, or log them.
The structural fix is to stop the shell from holding plaintext at all. Move values into a vault, hand the agent references, and inject the real value into the child process only when a command runs. Once there is no plaintext in the environment, there is nothing to inherit, echo, commit, or log, which is why this approach reaches all five surfaces while file-access rules reach one.
hasp is one working implementation of that pattern, built for this exact case: local vault, value-free references for the agent, exec-time injection into the child process, real-time redaction, and an append-only audit log of every access. Source-available (FCL-1.0), local-first, macOS and Linux, no account. Whichever tool you use, the test is the same: after a session, grep the transcript for a key prefix you know. If you find one, the value was in context, and the fix is to take it out of the shell.
Sources· cited above, in one place
- Anthropic security Vulnerability disclosure and Trust Center
- Knostic Research on AI code editor secret leakage (Claude Code, Cursor)
- GitGuardian State of Secrets Sprawl report
- Model Context Protocol Specification
- OWASP GenAI Security Project Top 10 for Agentic Applications 2026 (ASI01-ASI10)
- Fair Core License FCL-1.0-ALv2 text
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.