Secret Accidentally Included in Prompt Context

An API key, password, or token ended up inside a prompt. How to rotate it fast, trace where it traveled, and stop secrets from entering the model context again.

You paste a failing request’s error log into an AI chat to get debugging help, and the log contains Authorization: Bearer sk-proj-.... That token is now in the model’s context, and almost certainly in your API provider’s logs, your app’s chat-history table, and any monitoring that records prompt payloads. Deleting the message from the UI does not undo this.

Fastest fix: rotate the exposed credential right now, before you investigate anything else. A leaked key stays valid until you revoke it. A key committed or pasted in 2024 still works in June 2026 unless someone manually rotated it. Rotation is the only action that actually closes the exposure; everything after that is cleanup and prevention.

This article covers, in order: rotate, then find everywhere it traveled, then add controls so it cannot happen again.

Which bucket are you in?

The right cleanup depends on how the secret entered the context. Identify your case first.

How it got inWhere to lookHighest-leverage control
Pasted an error log / config snippet by handChat history, provider logsPre-send scanner on user messages
Agent read .env / secrets.yaml as project contextAgent file-access log, tool-call transcriptFile-path blocklist (see Claude Code caveat below)
Tool output (cat, env, printenv) appended to next turnTool-call transcriptRedact tool output before it re-enters context
Secret interpolated into a system-prompt templateSource code that builds the promptLint rule + server-side credential handling
CI/CD dumped env vars into a prompt-building stepCI logs, pipeline configStrip printenv / masked CI vars from prompt inputs
Persisted to chat-history DB / cache / training exportDatabase, Redis, object storage, log shippersRedact before write

The exposure surface is the same across vendors: the prompt payload should be treated as a log entry that may be retained for years.

Common causes

1. Pasting error logs that contain authorization headers

HTTP error logs commonly include full request and response headers. Authorization: Bearer sk-..., X-API-Key: ..., or Cookie: session=... appear inline. Once that log is in the prompt, every secret in it is exposed.

How to spot it: Before pasting any log into an AI chat, grep it first:

grep -iE "(authorization|x-api-key|bearer|password|secret|token|cookie|aws_|sk-)" /path/to/error.log

If any line returns a hit, redact it before pasting.

2. Agent reads .env or config files as part of project context

An AI coding assistant given broad file-system access “to understand the project” reads .env, config/production.yaml, or terraform.tfvars, and those files contain production secrets.

How to spot it: Check which files the agent touched during the session. In Claude Code, look at the tool-call transcript for Read calls; in Cursor, check the context pills attached to the message.

Important June 2026 caveat: A permissions.deny rule for .env in Claude Code is not a hard guarantee. Multiple open issues filed between August 2025 and February 2026 (for example anthropics/claude-code#24846) report that read-deny rules can fail silently and the file gets read anyway. Treat the blocklist as defense-in-depth, not a wall. The only reliable protection is to not have live secrets readable from the working directory at all (use a secrets manager, keep .env outside the repo root, or use placeholder values locally).

3. System prompt template includes a secret via string interpolation

A developer builds the system prompt dynamically and interpolates a secret:

const systemPrompt = `You are a support agent for our API. The internal admin key is ${ADMIN_KEY}. Use this for escalations.`;

How to spot it: Grep every file where prompts are constructed for process.env., os.environ, or any variable name containing key, secret, token, password.

4. Tool-call output is appended to the next turn verbatim

An agent runs a bash tool that executes cat config.yaml, env, or aws configure list, and the orchestration layer appends the raw output into the next turn’s context.

How to spot it: Log all tool outputs and run secret-pattern scans over them before they re-enter the conversation. Confirm whether credentials are passed as plain strings or as opaque handles.

5. CI/CD pipeline passes environment variables into prompt templates

An automated pipeline builds prompts from CI environment variables, and a debug step (printenv, echo $VARIABLE, or a masked-but-logged secret) feeds into the prompt-building stage.

How to spot it: Review every echo $VARIABLE and printenv in your CI scripts and check whether their output is consumed by a prompt-building step. Note that most CI systems mask secrets in their own UI but do not mask them inside data you pass to an external API.

6. Chat history persistence captures the secret permanently

Even after you delete the message from the UI, the secret may remain in a database row, a feedback export, a Redis cache, or your provider’s operational logs. Deletion from the UI is not deletion from the system.

How to spot it: Search your chat-history storage (database tables, object storage, log-shipping destinations) for the value and confirm who has read access.

Shortest path to fix

Step 1: Rotate the exposed credential immediately

This step cannot wait for the investigation. Revocation is instant and irreversible, which is exactly what you want.

# OpenAI: https://platform.openai.com/api-keys
#   Click the trash icon next to the exposed key, then create a new one.
#   Project-scoped keys (sk-proj-...) only affect the one project; rotate that project's key.

# GitHub: https://github.com/settings/tokens (or settings/personal-access-tokens for fine-grained)
#   Delete the exposed token, generate a replacement, re-authorize dependents.

# AWS access key pair:
aws iam delete-access-key --access-key-id AKIA_EXPOSED_KEY_ID --user-name svc-account-name
aws iam create-access-key --user-name svc-account-name

After rotating, update every dependent (CI secrets, deploy configs, running services) and confirm the old credential now fails. A 401/403 from the old key is your proof of revocation.

Step 2: Confirm whether the leaked key was actually used

Before assuming the worst, check if the credential was live and exercised during the exposure window. TruffleHog can verify a key against the live provider and tell you whether it still authenticates:

# Verify only confirmed-live credentials in a scan target
trufflehog filesystem ./exported-chat-logs --results=verified

Then review provider-side usage (OpenAI usage dashboard, AWS CloudTrail, GitHub audit log) for calls you did not make during the exposure window. Unexpected usage escalates this from “rotate and move on” to a security incident.

Step 3: Identify everywhere the secret traveled

# Application and web-server logs
grep -r "EXPOSED_KEY_VALUE" /var/log/app/ /var/log/nginx/

# Chat-history database (run against a read-only replica)
# SELECT COUNT(*) FROM chat_messages WHERE body LIKE '%EXPOSED_KEY_VALUE%';

# Cache (example: Redis)
# redis-cli --scan --pattern 'chat:*' | xargs -I{} redis-cli get {} | grep -F 'EXPOSED_KEY_VALUE'

# Also search your monitoring/SIEM index for the literal value.

Wherever you find it, redact or purge the stored value. The point is not to “clean it up so it’s safe” (you already rotated, so the value is dead) but to remove a dead secret that an auditor or scanner would still flag.

Step 4: Add a pre-send secret scanner to your prompt builder

Use patterns that match the current key formats. OpenAI’s default keys are project-scoped (sk-proj-...) and embed the literal T3BlbkFJ mid-token; GitHub fine-grained PATs use github_pat_. The old sk-[A-Za-z0-9]{20,} pattern alone misses today’s keys.

const SECRET_PATTERNS = [
  /sk-(proj|svcacct|admin)-[A-Za-z0-9_-]{20,}/g, // current OpenAI keys
  /\bT3BlbkFJ[A-Za-z0-9_-]{20,}/g,               // OpenAI key body marker
  /\bgithub_pat_[0-9A-Za-z_]{82}\b/g,            // GitHub fine-grained PAT
  /\bghp_[A-Za-z0-9]{36}\b/g,                    // GitHub classic PAT
  /\bAKIA[A-Z0-9]{16}\b/g,                       // AWS access key id
  /\bAIza[0-9A-Za-z_-]{35}\b/g,                  // Google API key
  /xoxb-[0-9]{11,}-[A-Za-z0-9-]+/g,              // Slack bot token
  /-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----/g,
  /(?:password|api[_-]?key|secret)\s*[:=]\s*\S+/gi,
];

function containsSecret(text: string): boolean {
  return SECRET_PATTERNS.some((re) => re.test(text));
}

function buildPromptSafe(userMessage: string, context: string): string {
  if (containsSecret(context)) {
    throw new Error("Context contains a potential secret. Redact before including in prompt.");
  }
  if (containsSecret(userMessage)) {
    logger.warn({ event: "user_message_contains_secret" });
    // Block or redact depending on policy.
  }
  return `${systemPrompt}\n\nContext:\n${context}\n\nUser: ${userMessage}`;
}

Regex shape-matching catches the obvious cases but is necessary, not sufficient. Pair it with a real scanner (gitleaks/TruffleHog) in CI for full-history coverage.

Step 5: Block secret-pattern file paths from agent access

const BLOCKED_FILE_PATTERNS = [
  /\.env(\.\w+)?$/,
  /secrets\.(yml|yaml|json)$/i,
  /credentials\.json$/i,
  /terraform\.tfvars(\.json)?$/i,
  /\.netrc$/,
  /.*\.pem$/,
  /.*_rsa(\.pub)?$/,
  /.*\.p12$/,
];

function canReadFile(path: string): boolean {
  return !BLOCKED_FILE_PATTERNS.some((re) => re.test(path));
}

For Claude Code specifically, also set deny rules in settings.json, but remember the June 2026 caveat from cause 2 (these can fail silently, so do not rely on them alone):

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}

Step 6: Redact secrets before the prompt is stored

function redactSecretsFromPrompt(prompt: string): string {
  let redacted = prompt;
  for (const pattern of SECRET_PATTERNS) {
    redacted = redacted.replace(pattern, "[REDACTED_SECRET]");
  }
  return redacted;
}

// Store only the redacted version in your chat-history database.
const safePromptForStorage = redactSecretsFromPrompt(fullPrompt);
await db.chatMessages.create({ body: safePromptForStorage, sessionId });

Step 7: Add a pre-commit hook so secrets never reach the code that builds prompts

Note: gitleaks protect was deprecated in gitleaks v8.19.0 and replaced by the gitleaks git command. Use the current syntax:

# Install gitleaks (current command model: git / dir / stdin)
brew install gitleaks

cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
gitleaks git --pre-commit --staged --redact --no-banner
if [ $? -ne 0 ]; then
  echo "gitleaks: secrets detected in staged files. Commit blocked."
  exit 1
fi
EOF
chmod +x .git/hooks/pre-commit

For full-history sweeps in CI, run gitleaks git --redact over the whole repo, or trufflehog git file://. --results=verified to flag only credentials that are still live.

How to confirm it’s fixed

  1. The old credential now returns 401/403 (revocation confirmed).
  2. Provider-side audit logs show no unexpected usage during the exposure window.
  3. A fresh scan of your chat-history store, cache, and logs no longer returns the value.
  4. The pre-send scanner blocks a deliberately planted test secret (sk-proj-TESTplaceholder...) in a staging prompt.
  5. gitleaks git --redact over the repo returns zero findings.

If all five pass, the incident is closed. If step 2 surfaced unexpected usage, treat it as a breach and follow your incident-response process.

Prevention

  • Treat every prompt payload as a log entry retained for years. Never include a live secret in any prompt.
  • Run a secret scanner on prompt content before it hits the model API, using patterns that match current key formats (sk-proj-, github_pat_, and the T3BlbkFJ body marker).
  • Block agent file access to anything whose path looks like a credential file, and do not rely on permissions.deny alone for .env in Claude Code as of June 2026.
  • Store prompt and response payloads with secrets redacted.
  • Run gitleaks git or trufflehog in pre-commit hooks and CI.
  • Never interpolate raw environment variables into system-prompt templates. If a tool needs a credential, hand it to a server-side tool handler, not the model.
  • Keep a one-page rotation runbook per secret type so any engineer can revoke in under 10 minutes.

FAQ

Q: I deleted the message from the chat UI. Is the secret gone? A: Almost certainly not. UI deletion typically removes only the user-facing record. The database row, provider-side retention, log aggregation, and monitoring captures can all still hold the value. Assume it is compromised and rotate.

Q: My provider says they do not train on API data. Does that make the prompt safe? A: No. The no-training policy covers training datasets, not operational logging for abuse detection, debugging, or legal retention. Assume prompts may be retained for some period and design accordingly.

Q: I told Claude Code to deny reading .env. Why did it read it anyway? A: This is a known issue tracked in multiple reports between August 2025 and February 2026: read-deny rules can fail silently. Use the deny rule as one layer, but the reliable fix is to keep live secrets out of the working directory entirely (secrets manager, or local placeholder values).

Q: Should I use a secrets manager instead of .env files? A: Yes. AWS Secrets Manager, HashiCorp Vault, and GCP Secret Manager inject secrets at runtime via API rather than persisting them in files or environment dumps, and every access is auditable. That removes the most common path a secret takes into a prompt.

Q: How do I know whether the leaked key was actually abused? A: Verify it against the provider with trufflehog --results=verified to confirm it was live, then check provider audit logs (OpenAI usage dashboard, AWS CloudTrail, GitHub audit log) for calls during the exposure window that you did not make.

Tags: #ai-security #prompt-injection #Troubleshooting