PricingSign inDownload

Blog / claude code

Pasted an API key into Claude Code? Do this now

Where a key you pasted into Claude Code or Codex ends up on your disk, how to remove every copy, and a hook that stops the next paste before it's sent.

On this page
  1. First: rotate the key
  2. Where the key ends up on your disk
  3. Find and remove every copy
  4. Stop the next paste before it's sent
  5. Give the agent the key without pasting it
  6. The short version
  7. Try it with your team

It happens to everyone once. You're setting something up, the agent asks for a key, and you paste it straight into the chat. The agent uses it and the job gets done. A few minutes later it hits you that the key is now in a conversation, and you don't know where that conversation is stored.

We pasted fake keys into Claude Code 2.1.312 and Codex 0.164 and searched the disk afterwards. Here's where each copy went, how to remove them, and a hook that stops the next paste before it leaves your machine.

First: rotate the key#

Do this before anything else. What you paste goes to the model provider as part of the conversation, so cleaning your own disk doesn't undo that. Open the dashboard of whatever issued the key (OpenAI, Stripe, AWS, GitHub, your database host), create a new key, update wherever the old one is used, and revoke the old one. Once it's revoked, every copy left anywhere is harmless.

Claude itself said as much when we tested. Its whole reply to a prompt with a fake key was: "Since it's now in this chat transcript, rotate it if it's a real credential."

Where the key ends up on your disk#

Claude Code keeps every session as a file under ~/.claude/projects/, one folder per project, one .jsonl file per session. With a one-shot claude -p prompt, that's the only copy we found. In an interactive session there was a second one: ~/.claude/history.jsonl, which stores what you typed so you can bring it back with the up arrow. If you set CLAUDE_CONFIG_DIR, both live in that folder instead of ~/.claude.

Claude Code deletes old session files after 30 days by default (the cleanupPeriodDays setting), so a pasted key sits there for a month unless you remove it.

Codex writes each session to ~/.codex/sessions/<year>/<month>/<day>/rollout-….jsonl. Our codex exec test left the key there. Interactive Codex sessions also save to ~/.codex/history.jsonl, which the history.persistence setting controls.

Both tools keep these files on your machine. They're how resuming a session works, so deleting the whole folder costs you every past conversation. You don't need to: you can remove just the key.

Find and remove every copy#

Put the key in a variable for this one terminal session, so you're not pasting it into more commands than necessary:

bash
read -rs KEY        # paste the old key, press Enter (nothing is shown)
export KEY
grep -rl -- "$KEY" ~/.claude ~/.codex

That lists every file holding it. To replace the key with [removed] inside each file, without touching anything else:

bash
grep -rl -- "$KEY" ~/.claude ~/.codex | xargs perl -pi -e 's/\Q$ENV{KEY}\E/[removed]/g'
grep -rl -- "$KEY" ~/.claude ~/.codex      # should print nothing now
unset KEY

We ran exactly this over four sessions (three Claude Code, one of them blocked by the hook below, and one Codex). The second grep found nothing, and the session files were still valid, so they can still be resumed. \Q…\E makes perl treat the key as plain text, so characters like + or / in it don't break the match.

Also check the places outside the agent where the key may have landed: your project's .env (fine, that's where it should be, as long as it's in .gitignore), a file the agent wrote it into, and your git history if the agent committed that file. git log -p -S "$KEY" (with the variable still set) finds a key that was ever committed. If it was, rotating it is the fix, since rewriting shared history causes its own problems.

Stop the next paste before it's sent#

Claude Code runs a UserPromptSubmit hook on every prompt before Claude sees it. If the hook exits with code 2, the prompt is not sent and the hook's message is shown to you. That's a good place to catch the common key shapes.

Save this as .claude/hooks/block-keys.sh (or in ~/.claude/hooks/ to use it in every project):

bash
#!/bin/bash
# Exit 2 stops the prompt before Claude sees it; stderr is shown to you.
prompt=$(jq -r '.prompt // empty')
if printf '%s' "$prompt" | grep -Eq '(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|AKIA[0-9A-Z]{16}|xox[abpr]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{35})'; then
  echo "That looks like a secret key. Put it in .env and refer to it by name." >&2
  exit 2
fi
exit 0

Make it executable with chmod +x, then add it to .claude/settings.json (or ~/.claude/settings.json for every project):

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-keys.sh" }
        ]
      }
    ]
  }
}

For the user-level version, point command at the full path of the script instead of $CLAUDE_PROJECT_DIR. The patterns cover the usual prefixes: OpenAI and Anthropic style sk- keys, GitHub tokens, AWS access key ids, Slack tokens and Google API keys. Add your own provider's prefix if it has one. The script needs jq.

Here's what happened when we pasted a fake key with the hook in place. Claude Code showed "UserPromptSubmit operation blocked by hook" with our message, Claude never replied, and the prompt stayed in the input box so we could edit the key out and send the rest. The key did not reach history.jsonl.

One catch we found: the session file still recorded the block, and that note includes the original prompt, key and all. So the key never left your machine, but a local copy remains. The grep and perl lines above clean it up.

Give the agent the key without pasting it#

The habit that prevents all of this: keys go in a file the agent's tools read, never in the chat.

bash
# .env (and .env in .gitignore)
STRIPE_SECRET_KEY=...

Then say "the Stripe key is STRIPE_SECRET_KEY in .env". The agent's code and commands read it from there, and the conversation only ever contains the name. If you'd rather the agent never reads .env itself, see keeping Claude Code away from .env files: your app still loads the key at runtime, and the agent works with the name.

The short version#

  • Rotate the key first. The paste already went to the provider; nothing on your disk changes that.
  • Claude Code keeps it in ~/.claude/projects/…/<session>.jsonl (and ~/.claude/history.jsonl for interactive sessions) for 30 days by default. Codex keeps it in ~/.codex/sessions/… and ~/.codex/history.jsonl.
  • grep -rl finds every copy, and perl -pi replaces just the key without breaking the sessions.
  • A UserPromptSubmit hook blocks the prompt before Claude sees it. Clean the one local note it leaves.
  • Keep keys in .env and tell the agent the variable's name.

When your teammates' agents work in the same project, set the hook up in the project's .claude/settings.json and commit it, so everyone gets it. More on that in setting up Claude Code for a team.