The most common way an AWS key reaches a model isn’t an attack. An engineer pastes a stack trace that includes a connection string, or an agent runs cat config/local.yaml to “understand the setup”, and the key is now in a prompt, in the vendor’s request logs and possibly in the next patch the agent writes.
A secret-aware setup has three layers. Keep secret files out of reach with the tools’ own ignore and deny rules. Filter what still gets through, redacting secrets from prompts and file context before they leave your machine. Check what comes back, so a model can’t write a live credential into code or a PR. And when something does leak, rotate first. This guide covers each layer with tested code, and it merges our earlier bootcamp lesson on secret-aware context filters into this guide.
⚡ TL;DR
- Cursor’s
.cursorignoreblocks files from Agent, Tab, Inline Edit and @-mentions, and.env*plus everything in.gitignoreis ignored by default. But per Cursor’s docs, the agent’s terminal and MCP tools aren’t blocked, and protection isn’t guaranteed.- Claude Code uses
Readdeny rules insettings.json. They cover built-in file tools and recognized commands likecat, but not scripts that open files themselves. Its sandbox gives OS-level enforcement.- Add a redaction filter with high-precision detectors (AWS key IDs, GitHub and Slack token prefixes, PEM blocks) plus an entropy pass, using stable placeholders.
- Block prompts containing secrets with a
UserPromptSubmithook, and refuse model output that contains a credential.- If a key leaked: deactivate it, check its last use and CloudTrail, then replace it, preferably with a role.
Who this is for: developers and platform teams using AI editors and coding agents on code that sits next to real credentials. Examples use Python, Claude Code and Cursor configuration, and the AWS CLI. Tool behavior described here was checked against Cursor’s and Claude Code’s documentation in October 2026; both change often, so recheck before you rely on a specific rule.
Where secrets enter AI context
Map the paths before choosing controls. A coding assistant can see a secret through:
- Files it reads: indexing, @-mentions, the agent’s file tools.
- Your prompt: pasted logs, stack traces, curl commands with headers.
- Editor state: open buffers and selections shared with the assistant.
- Tool output: a terminal command such as
envorcat, or an MCP tool that returns configuration.
Ignore and deny rules cover the first path well, and the others only partly. That’s why the filter and the output check exist.

Layer 1: Keep secret files out of reach
Cursor
Cursor reads a .cursorignore file in the project root, using .gitignore syntax. Files listed there are blocked from Agent, Tab, Inline Edit and @-mention references. Cursor also ignores everything in .gitignore and a default list that includes .env* and lockfiles, and you can add global ignore patterns in user settings. Cursor’s documentation is explicit about two limits: the terminal and MCP tools used by Agent can’t block access to ignored files, and complete protection isn’t guaranteed. So list files that hold secrets but aren’t gitignored:
# Cursor already ignores .env* and everything in .gitignore by default.
# List secret-bearing files that ARE committed or not gitignored:
**/*.pem
**/*.key
**/credentials.json
deploy/secrets/
terraform/*.tfvarsTest a pattern with git check-ignore -v <file>, which Cursor’s docs recommend because the syntax is the same.
Claude Code
Claude Code uses permission rules in .claude/settings.json. A Read deny rule blocks its built-in file tools, recognized file commands in Bash such as cat, head and tail, and redirections, and Claude Code makes a best-effort attempt to apply it to @file mentions and IDE context. The docs warn that it doesn’t cover commands that read files without naming them, such as grep -r, or scripts that open files themselves. For OS-level enforcement, enable the sandbox. A .claudeignore file has no effect.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/*.pem)",
"Read(./secrets/**)"
]
},
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block_secrets_in_prompt.py",
"onFailure": "block"
}
]
}
]
}
}The hooks section wires in the prompt filter from layer 2. Commit this file so the whole team gets the same rules.
The strongest version of layer 1 is not having the secret in the working tree at all. Load credentials at runtime from AWS Secrets Manager or your identity provider, and use short-lived role credentials instead of access keys in local profiles.
Layer 2: Redact what still gets through
The filter below runs on any text about to enter a model’s context: a prompt, a file being attached, a tool result your own agent passes back. It combines high-precision detectors for formats with known prefixes, a keyword rule for assignments like password = "...", and an entropy pass for unknown token shapes, which skips lockfiles full of hashes.
"""Redact secrets from anything about to enter a model's context, and refuse output that re-emits them."""
import fnmatch
import hashlib
import math
import re
from collections import Counter
DENY_PATHS = ["**/.env", "**/.env.*", "**/*.pem", "**/*.key", "**/id_rsa", "**/id_ed25519",
"**/.aws/credentials", "**/credentials.json", "**/secrets.*"]
ENTROPY_SKIP = ["**/package-lock.json", "**/pnpm-lock.yaml", "**/yarn.lock", "**/*.lock", "**/go.sum"]
DETECTORS = [
("aws_access_key_id", re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")),
("github_token", re.compile(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}\b|\bgithub_pat_[A-Za-z0-9_]{22,}\b")),
("slack_token", re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b")),
("private_key", re.compile(r"-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----.*?-----END (?:[A-Z]+ )?PRIVATE KEY-----", re.S)),
("assigned_secret", re.compile(
r"(?i)\b(?:secret|password|passwd|token|api[_-]?key|aws_secret_access_key)\b\s*[:=]\s*['\"]?([^\s'\"]{8,})")),
]
TOKENISH = re.compile(r"[A-Za-z0-9/+=_-]{32,}")
def _matches(path: str, patterns: list) -> bool:
p = "/" + path.lstrip("/")
return any(fnmatch.fnmatch(p, pat) or fnmatch.fnmatch(p, pat.replace("**/", "*/")) for pat in patterns)
def entropy(s: str) -> float:
n = len(s)
return -sum(c / n * math.log2(c / n) for c in Counter(s).values()) if n else 0.0
def placeholder(kind: str, value: str) -> str:
# Same secret -> same placeholder, so the model can still reason about "the key in config.py".
return f"[REDACTED:{kind}:{hashlib.sha256(value.encode()).hexdigest()[:8]}]"
def redact(text: str, path: str = "", threshold: float = 4.5) -> tuple:
if path and _matches(path, DENY_PATHS):
return f"[BLOCKED: {path} is on the deny list]", [("blocked_path", path)]
events = []
def sub(kind):
def repl(m):
value = m.group(1) if m.lastindex else m.group(0)
events.append((kind, path))
return m.group(0).replace(value, placeholder(kind, value))
return repl
for kind, rx in DETECTORS:
text = rx.sub(sub(kind), text)
if not (path and _matches(path, ENTROPY_SKIP)):
def high_entropy(m):
s = m.group(0)
if s.startswith("[REDACTED") or entropy(s) < threshold:
return s
events.append(("high_entropy", path))
return placeholder("high_entropy", s)
text = TOKENISH.sub(high_entropy, text)
return text, events
def assert_no_secret_egress(model_output: str) -> None:
"""Run on model output before it's written to disk, a PR or a tool call."""
for kind, rx in DETECTORS[:4]: # high-precision detectors only
if rx.search(model_output):
raise PermissionError(f"model output contains a secret ({kind}); refusing to apply")The prefixes come from the vendors: AWS access key IDs start with AKIA, and temporary STS keys with ASIA. GitHub documents ghp_ (classic PAT), github_pat_ (fine-grained), gho_, ghu_, ghs_ and ghr_. GitHub has also started rolling out a new format for App installation tokens, so treat any length assumption in your patterns as something to re-test.
Placeholders are stable: the same secret always becomes the same [REDACTED:kind:hash]. The model can still say “the key on line 2 is hard-coded; move it to Secrets Manager” without ever seeing it. Here’s the filter on a typical config file, using AWS’s documented example key:
from secretfilter import assert_no_secret_egress, redact
AKID = "AKIA" + "IOSFODNN7EXAMPLE" # AWS's documented example key ID
SRC = f'''import boto3
AWS_ACCESS_KEY_ID = "{AKID}"
aws_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
client = boto3.client("s3")
'''
out, events = redact(SRC, "app/config.py")
print(out)
print(events)
assert AKID not in out and "wJalrXUtnFEMI" not in out and "boto3.client" in out
assert redact("x", "services/api/.env.production")[1] == [("blocked_path", "services/api/.env.production")]
again, _ = redact(SRC, "app/config.py")
assert again == out # stable placeholders
lock, ev = redact('"integrity": "sha512-' + "Zx9" * 20 + '"', "web/package-lock.json")
assert ev == [] # lockfile hashes are not secrets
try:
assert_no_secret_egress(f"Use key {AKID} in the client")
except PermissionError as e:
print("blocked egress:", e)Output from that test run:
import boto3
AWS_ACCESS_KEY_ID = "[REDACTED:aws_access_key_id:1a5d44a2]"
aws_secret_access_key = "[REDACTED:assigned_secret:78314b11]"
client = boto3.client("s3")
[('aws_access_key_id', 'app/config.py'), ('assigned_secret', 'app/config.py')]
blocked egress: model output contains a secret (aws_access_key_id); refusing to applyBlock prompts before they’re sent
Claude Code runs UserPromptSubmit hooks before a prompt is processed. A hook that returns "decision": "block" stops the prompt from reaching Claude, and the reason is shown to you, not added to the context. This hook reuses the filter:
#!/usr/bin/env python3
"""Claude Code UserPromptSubmit hook: stop prompts that contain secrets before they reach the model."""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", "."), "tools"))
from secretfilter import redact # noqa: E402
event = json.load(sys.stdin)
_, findings = redact(event.get("prompt", ""))
if findings:
kinds = sorted({kind for kind, _ in findings})
print(json.dumps({
"decision": "block",
"reason": f"Prompt blocked: it looks like it contains {', '.join(kinds)}. "
"Remove the value (or reference the secret by name) and resubmit.",
"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "suppressOriginalPrompt": True},
}))
sys.exit(0)The default timeout for this hook type is 30 seconds, and by default a hook that times out is cancelled and the prompt still goes through. That’s why the settings above set "onFailure": "block", which makes a failed or timed-out check block the prompt instead (this needs Claude Code v2.1.295 or later; the default is "continue"). Keep the check fast either way. Tool output is a different matter: a PostToolUse hook runs after the tool already ran, so it can’t stop that output reaching the model. Prevent risky commands with deny rules or a PreToolUse hook instead.
Layer 3: Check what comes back
Models copy what they see. If a key ever made it into context, the next patch may hard-code it “to fix the error”. Run assert_no_secret_egress on generated output before your agent writes files, opens a PR or calls a tool, and keep a scanner such as gitleaks in pre-commit and CI (gitleaks git scans the repo history, gitleaks dir scans files, and gitleaks stdin scans piped text). The egress check uses only the high-precision detectors, so it doesn’t block legitimate output on entropy false positives.
When a key leaks anyway: rotate first
Treat any key that reached a prompt as exposed. Deactivate it immediately (deactivation is reversible, deletion isn’t), then investigate:
#!/usr/bin/env bash
# Rotate first, investigate second. Usage: rotate_leaked_key.sh <iam-user> <access-key-id>
set -euo pipefail
USER_NAME="$1"; KEY_ID="$2"
# 1. Stop the leaked key working now (reversible, unlike delete).
aws iam update-access-key --user-name "$USER_NAME" --access-key-id "$KEY_ID" --status Inactive
# 2. When was it last used, and for what?
aws iam get-access-key-last-used --access-key-id "$KEY_ID"
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue="$KEY_ID" \
--max-results 50 --query 'Events[].[EventTime,EventName,EventSource]' --output table
# 3. Issue a replacement only if a long-lived key is really needed (prefer roles), then delete the old one.
# aws iam create-access-key --user-name "$USER_NAME"
# aws iam delete-access-key --user-name "$USER_NAME" --access-key-id "$KEY_ID"lookup-events searches CloudTrail’s event history, which covers the past 90 days of management events in the current Region; check other Regions and your CloudTrail trail or data lake for anything older or for data events. Replace a long-lived user key with a role where you can. For secrets in Secrets Manager, trigger rotation with aws secretsmanager rotate-secret --secret-id <name> once rotation is configured.
Worked example: a stack trace with a key in it
- A developer pastes a failing boto3 call into Claude Code. The traceback includes the client config with an access key ID.
- The
UserPromptSubmithook blocks the prompt with “it looks like it contains aws_access_key_id”. The developer removes the line and resubmits; nothing reached the model. - Separately, the team’s PR bot attaches
app/config.pyto its context. The filter replaces both credentials with placeholders, and the bot suggests moving them to Secrets Manager. - The bot’s patch passes the egress check and gitleaks in CI.
- Because the key was already in a committed file, the team runs the playbook anyway: deactivate, review CloudTrail, switch the service to a role and delete the key.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Agent read an ignored file anyway | It used the terminal, an MCP tool or a script | Expected per the docs; move the secret out of the repo, use the Claude Code sandbox, and keep the filter on tool results |
| Too many redactions in normal code | Entropy threshold too low, or lockfiles and hashes scanned | Raise the threshold, add files to the skip list, rely on prefix detectors |
| A real token wasn’t caught | Unknown format with no prefix | Add a detector; vendors’ secret-scanning partner formats are a good source |
| Prompt hook never fires | Wrong path, missing python3, or invalid JSON in settings | Run the hook by hand with a sample JSON on stdin; check the debug log |
| Hook slows every prompt | Heavy scanning in the hook | Keep it to regexes; the default timeout for this hook is 30 seconds |
Production checklist
- ☐ No long-lived credentials in the working tree; runtime secrets from Secrets Manager; roles over access keys
- ☐
.cursorignoreand global ignore patterns for secret files that aren’t gitignored - ☐ Claude Code
Readdeny rules committed; sandbox enabled where agents run commands - ☐ Redaction filter on prompts, attachments and tool results your own agents pass to models
- ☐ Prompt-blocking hook; egress check before any write or PR
- ☐ gitleaks in pre-commit and CI
- ☐ Rotate-first playbook documented and rehearsed
FAQ
Isn’t .gitignore enough?
No. It keeps files out of commits, and Cursor also honors it for AI access, but the agent’s terminal can still print the file, and secrets often live in files that are committed by mistake.
Can’t the model vendor filter secrets server-side?
By then the secret has already left your machine. Filter before sending; treat vendor-side controls as a backup.
Should I hash or remove secrets?
Replace them with a stable placeholder that includes a short hash. It’s not reversible, and it lets the model refer to “the same key” consistently.
Does the Claude Code deny rule stop python -c "print(open('.env').read())"?
No. The docs say deny rules don’t apply to subprocesses that open files themselves. Use the sandbox for OS-level enforcement.
Official documentation
- Cursor: ignore files
- Claude Code: permissions and hooks
- GitHub: token formats
- AWS IAM identifiers (key prefixes)
- Managing access keys for IAM users
- Rotate AWS Secrets Manager secrets
Related on CheatCoders: PrivateLink and secret hygiene for coding agents · PII redaction before embeddings
Last updated on October 10, 2026
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Lambda Code Signing: Enforce CI-Built Artifacts in Every Account - CheatCoders