GUIDE · HOW-TO 11 min ·

Can my agent see my secrets, an auditDrop canaries. Run commands. See what leaks.

Most developers assume the agent reads only what they typed. That assumption is wrong. Here is how to test it.

TL;DR· the answer, in twenty seconds

What: Every major coding agent inherits your shell environment when it launches, which means every exported secret is inside the agent's context from the first keystroke. Several tools also persist some of that environment to on-disk state files.

Fix: Run the canary audit below, check the grep sweep, then eliminate long-lived exports from your shell in favor of process-scoped injection.

Lesson: Any tool that inherits an ambient environment is a potential carrier. The audit tells you what actually happened in your specific setup. The fix is architectural: stop exporting secrets to the shell.

Most developers assume the coding agent reads only the files they opened and the prompts they typed. The agent also inherits the full process environment of the shell that launched it. That includes every exported variable in your .zshrc, your ~/.aws/credentials sourced via a script, the GITHUB_TOKEN you pasted for a quick test three months ago and never unset, and anything the agent's shell integration injected at startup.

The agent reads more than your prompt. The real question is what it does with your secrets afterward: mention them in a response, write them to a log, persist them in a state file, send them upstream in a telemetry payload.

Knostic's February 2026 disclosure found Claude Code's settings.local.json capturing env-var values and landing in published npm packages at a rate of roughly 1-in-13. That was one tool and one output path. The other tools in your workflow have their own state files, their own log paths, and their own capture behaviors. This audit runs the same test against all four.

It takes about 30 minutes. You need a terminal, the tools you already use, and optionally a DNS provider that logs resolver queries.

What to know in 60 seconds

  • Every coding agent inherits your shell environment. STRIPE_SECRET_KEY exported in your shell is visible to Claude Code, Cursor, Codex CLI, and Aider.
  • Several tools write per-project state files to disk. Those files can contain session metadata, recent context, or raw environment data.
  • macOS writes agent process activity and crash information to ~/Library/Logs/. Log entries can include command-line arguments and environment variables.
  • A canary secret is a unique value you control. You drop it in the environment, run the agent, then grep every state file the agent might have written. If the canary appears, you know the path it took.
  • DNS canaries extend that coverage to background network calls: if the agent resolves a hostname you planted in an env var, you see the query in your resolver logs even if no HTTP request ever completes.

The setup

Create canary tokens

Open a terminal in a scratch directory, not a real project:

mkdir ~/agent-audit && cd ~/agent-audit
git init

# Generate two unique canary values
CANARY_ENV=$(uuidgen)
CANARY_DNS="audit-$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -c1-8)"

echo "Canary env token: $CANARY_ENV"
echo "Canary DNS label: $CANARY_DNS"

# Export them as real-looking variable names
export AUDIT_STRIPE_KEY="sk_live_${CANARY_ENV}"
export AUDIT_DB_URL="postgres://admin:${CANARY_ENV}@${CANARY_DNS}.audit.example.com/prod"

Write both values to a local file so you can search for them later:

echo "$CANARY_ENV" > ~/agent-audit/canary.txt
echo "$CANARY_DNS" >> ~/agent-audit/canary.txt

The canary approach works, but it assumes secrets are in the environment for the agent to find. A broker like hasp removes that substrate: if the session inherits a reference rather than a raw value, the canary test produces a clean result by design rather than by luck.

AUDIT_DB_URL contains a hostname under your notional control. Any agent that resolves it produces a DNS query you can observe.

Set up process monitoring (optional but useful)

On macOS, open a second terminal and start an oslog stream before you launch any agent:

log stream --predicate 'process == "claude" OR process == "cursor" OR process == "node"' \
  --style json 2>/dev/null | grep -i "sk_live\|CANARY\|AUDIT_"

It streams system log entries in real time and prints any line containing your canary prefix. Leave it running through all four audit sections.

Audit Claude Code

In your scratch directory, start a Claude Code session and ask it directly:

cd ~/agent-audit
claude

At the prompt, ask:

Print the value of the environment variable AUDIT_STRIPE_KEY. Then list all environment variables whose names start with AUDIT_.

Claude Code will print the canary. It inherited your shell, so that is expected. The audit question is what happens to that value next.

After the session, check what Claude Code persisted:

# Check state files in the scratch project
find ~/agent-audit/.claude -type f 2>/dev/null
cat ~/agent-audit/.claude/settings.local.json 2>/dev/null

# Check global Claude dirs
find ~/.claude -type f -name '*.json' | sort
ls -la ~/.claude/

# Search for the canary
grep -r "$CANARY_ENV" ~/.claude ~/agent-audit/.claude 2>/dev/null
grep -r "AUDIT_STRIPE_KEY" ~/.claude ~/agent-audit/.claude 2>/dev/null

Check macOS logs:

# Logs from the current session (adjust the date)
log show --predicate 'process == "claude"' --last 30m 2>/dev/null | grep -i "AUDIT_\|sk_live"

# Claude app log directory
ls ~/Library/Logs/ | grep -i claude
find ~/Library/Logs -name '*claude*' -type f 2>/dev/null | xargs grep -l "$CANARY_ENV" 2>/dev/null

Check open file descriptors while Claude Code is running:

# In a separate terminal while the session is active
CLAUDE_PID=$(pgrep -x claude | head -1)
lsof -p "$CLAUDE_PID" 2>/dev/null | grep -v 'txt\|mem\|cwd\|rtd\|DEL' | grep -i 'cache\|log\|claude\|tmp'

Check the cache directory:

ls ~/.cache/claude/ 2>/dev/null
find ~/.cache/claude -type f | sort
grep -r "$CANARY_ENV" ~/.cache/claude 2>/dev/null

Audit Cursor

Cursor embeds a modified VS Code with its own agent runtime. Before launching it, confirm your canary is still exported:

echo $AUDIT_STRIPE_KEY

Open Cursor from the same terminal so it inherits the environment:

cursor ~/agent-audit

In the Cursor chat panel, ask:

Print the value of AUDIT_STRIPE_KEY. List all env vars starting with AUDIT_.

After the response, search Cursor's state directories:

# Cursor project state
find ~/agent-audit/.cursor -type f 2>/dev/null
cat ~/agent-audit/.cursor/settings.json 2>/dev/null

# Global Cursor dirs
ls ~/.cursor/ 2>/dev/null
find ~/.cursor -type f | sort

# macOS application support
find "$HOME/Library/Application Support/Cursor" -type f | sort 2>/dev/null

# Search for canary
grep -r "$CANARY_ENV" ~/.cursor ~/agent-audit/.cursor 2>/dev/null
grep -r "$CANARY_ENV" "$HOME/Library/Application Support/Cursor" 2>/dev/null

# Logs
ls ~/Library/Logs/ | grep -i cursor
find ~/Library/Logs -name '*cursor*' 2>/dev/null | xargs grep -l "$CANARY_ENV" 2>/dev/null

Check FDs while the Cursor agent is processing a prompt:

CURSOR_PID=$(pgrep -x "Cursor Helper" | head -1)
lsof -p "$CURSOR_PID" 2>/dev/null | grep -i 'cache\|log\|cursor\|tmp'

Knostic disclosed a separate leak in Cursor's .cursor/ directory in January 2026, three weeks before the Claude Code disclosure. The format differs from settings.local.json, but the exposure path is identical: a per-project directory that enters version control with the next unfiltered git add ..

Audit Codex CLI

OpenAI's Codex CLI is Node-based and runs in the terminal. Launch it from the same shell:

cd ~/agent-audit
codex

At the prompt:

What is the value of the environment variable AUDIT_STRIPE_KEY? List all variables starting with AUDIT_.

After the session, check Codex state:

# Project-level codex directory
find ~/agent-audit/.codex -type f 2>/dev/null

# Global codex dirs
ls ~/.codex/ 2>/dev/null
find ~/.codex -type f | sort

# Node process cache
ls ~/.cache/codex/ 2>/dev/null
find ~/.cache/codex -type f | sort

# Search for canary
grep -r "$CANARY_ENV" ~/.codex ~/agent-audit/.codex 2>/dev/null
grep -r "$CANARY_ENV" ~/.cache/codex 2>/dev/null

Check the Node process FDs while Codex is running:

CODEX_PID=$(pgrep -f "codex" | head -1)
lsof -p "$CODEX_PID" 2>/dev/null | grep -v 'txt\|mem' | grep -i 'cache\|log\|tmp'

Audit Aider

Aider writes several files by default: .aider.chat.history.md captures conversation history, .aider.input.history stores your typed input, and .aider.conf.yml can hold model config. Run it from the scratch repo:

cd ~/agent-audit
aider

At the prompt:

/run env | grep AUDIT_

Or ask it directly in natural language:

Print the value of the environment variable AUDIT_STRIPE_KEY.

After the session:

# Aider drops files in the project directory
find ~/agent-audit -name '.aider*' -type f
cat ~/agent-audit/.aider.chat.history.md 2>/dev/null

# If you have a global aider config
ls ~/.aider* 2>/dev/null
cat ~/.aider.conf.yml 2>/dev/null

# Search for canary
grep -r "$CANARY_ENV" ~/agent-audit/.aider* 2>/dev/null
grep "$CANARY_ENV" ~/.aider* 2>/dev/null

The .aider.chat.history.md file is the highest-risk path. If you asked Aider to print AUDIT_STRIPE_KEY and it answered, that answer is now in the chat history file, in plaintext, committed alongside your project if you run git add . without a .gitignore entry.

Reading what each tool persisted

After running all four sessions, do a single broad sweep:

CANARY=$(cat ~/agent-audit/canary.txt | head -1)

grep -r "$CANARY" \
  ~/.claude \
  ~/.cursor \
  ~/.codex \
  ~/.aider* \
  ~/.cache/claude \
  ~/.cache/codex \
  ~/agent-audit/.claude \
  ~/agent-audit/.cursor \
  ~/agent-audit/.codex \
  ~/agent-audit/.aider* \
  "$HOME/Library/Application Support/Cursor" \
  2>/dev/null

Note every file path in the output. For each file:

# Identify what the file is
file <path>
# Read it (most are JSON or markdown)
cat <path>

Cross-reference against this table of common state file locations:

Tool Project-scoped Global
Claude Code .claude/settings.local.json ~/.claude/, ~/.cache/claude/
Cursor .cursor/ ~/.cursor/, ~/Library/Application Support/Cursor/
Codex CLI .codex/ ~/.codex/, ~/.cache/codex/
Aider .aider.chat.history.md, .aider.input.history ~/.aider.conf.yml

DNS canaries

The AUDIT_DB_URL variable you set earlier contains a hostname: ${CANARY_DNS}.audit.example.com. If any agent resolves that hostname, a DNS query leaves your machine and you can observe it, even if the TCP connection never opens.

To run this test properly, you need a domain under your control with a logging DNS server. The simplest option is Burp Collaborator (if you have a Burp Suite Pro license) or Interactsh (open source):

# Install interactsh-client
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest

# Start a listener. It gives you a unique hostname.
interactsh-client

# Replace the hostname in your canary and re-export
export AUDIT_DB_URL="postgres://admin:${CANARY_ENV}@<your-interactsh-host>/prod"

# Re-run an agent session
claude

Ask the agent to "connect to the database at $AUDIT_DB_URL and describe the schema." Any lookup of your Interactsh hostname appears in the client terminal, timestamped, with the source IP.

A simpler but less reliable approach uses tcpdump:

# Capture DNS queries while the agent runs
sudo tcpdump -i any -n 'port 53' 2>/dev/null | grep "$CANARY_DNS"

This shows queries your machine sent, but it does not catch queries from a remote service. If the agent sends your prompt to a cloud backend and the backend resolves hostnames embedded in your credentials, tcpdump sees nothing. The Interactsh approach catches that case because the lookup reaches your listener wherever it originates.

What this audit won't catch

The steps above cover persistent state files, log files, and DNS resolution. Several exposure paths sit beyond what a local audit can reach.

In-memory secrets. Your canary value lives in the agent's process memory for the duration of the session. If the agent sends that context window to a cloud API (which Claude Code, Cursor, and Codex do by design), the model provider processes the full context. Provider-side data retention policies govern what happens next. Anthropic, OpenAI, and Cursor's backend (also OpenAI-based) each publish their policies. Read them for the models you use. No local audit can verify cloud-side behavior.

Screenshot and screen-recording uploads. Some IDE integrations can capture screen content and include it in the context. If your terminal with a secret visible is on screen and the integration captures it, the value travels upstream as image data. The grep sweep above will not find it.

Log exfiltration via MCP. If you use MCP servers, any server you installed can read files and make network requests. A compromised or malicious MCP server can read /proc/self/environ on Linux (or ps eww output on macOS) and send results to an external endpoint. The Snyk researchers demonstrated this class of attack in early 2026 for the MCP GitHub prompt-injection disclosure. The canary sweep above does not cover network payloads from third-party MCP servers.

Completions cached at the provider. Some providers cache prompt-completion pairs for performance. A canary in your context window can appear in a cached response served to a different user if the provider's cache key is insufficiently isolated. Rare, but provider cache-key bugs have appeared in web search tooling.

The audit covers what lands on disk. It does not cover what travels over the wire or stays in memory.

A pasteable checklist

## Agent secret audit checklist

Setup
[ ] Generated unique canary values with uuidgen
[ ] Exported canary as realistic-looking env vars (AUDIT_STRIPE_KEY, AUDIT_DB_URL)
[ ] Optionally set up Interactsh or tcpdump for DNS canary monitoring

Claude Code
[ ] Asked agent to print AUDIT_STRIPE_KEY
[ ] Checked ~/.claude/ and .claude/ for canary value
[ ] Checked ~/.cache/claude/ for canary value
[ ] Ran: log show --last 30m | grep AUDIT_

Cursor
[ ] Asked agent to print AUDIT_STRIPE_KEY
[ ] Checked ~/.cursor/ and .cursor/ for canary value
[ ] Checked ~/Library/Application Support/Cursor/ for canary value

Codex CLI
[ ] Asked agent to print AUDIT_STRIPE_KEY
[ ] Checked ~/.codex/ and .codex/ for canary value
[ ] Checked ~/.cache/codex/ for canary value

Aider
[ ] Asked agent to print AUDIT_STRIPE_KEY
[ ] Checked .aider.chat.history.md for canary value
[ ] Checked ~/.aider* for canary value

Broad sweep
[ ] Ran grep -r <canary> ~/.claude ~/.cursor ~/.codex ~/.aider* ~/.cache/claude ~/.cache/codex
[ ] Noted every file path where canary appeared
[ ] Confirmed .gitignore covers all agent state directories

Remediation
[ ] Removed long-lived secret exports from ~/.zshrc or ~/.bashrc
[ ] Added all agent state dirs to .gitignore and .npmignore
[ ] Switched to process-scoped secret injection for future sessions

What this means for your stack

The audit tells you which paths are hot in your specific setup today. Those paths shift when you update a tool, swap a model, or add an MCP server. Run it again after each change.

The structural fix is a runtime model where secrets are never long-lived exports in the first place. A local broker holds credentials in an encrypted store. Agents request access per-session. The broker injects values into child processes at exec time and removes them when the process exits. The agent's context window sees a reference or nothing at all. A state file written after a session that ran this way contains nothing worth taking.

hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, hand the next session a reference instead of a key. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

The ambient export model is the risk surface. The canary audit shows you how wide that surface is right now. Shrinking it requires moving secrets out of the environment and into something that hands them out one process at a time.

Sources· cited above, in one place

NEXT STEP~90 seconds

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.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index