| layout | default | ||
|---|---|---|---|
| title | Security Checklist | ||
| parent | Part VI — Reference | ||
| nav_order | 6 | ||
| redirect_from |
|
Detecting secrets, permission gaps, and injection risks in your ~/.claude/ setup.
Your Claude Code configuration is executable surface area: hooks run shell, MCP servers inherit environment, skills can carry hidden directives, and permissions.allow entries gate every tool call. This chapter lists what to scan for, how to scan, how to automate it, and what to do when something leaks.
A mature setup (dozens of rules, skills, and hooks) accumulates risk silently:
- A hook written last month may shell out with
rm -funder>/dev/null - An MCP server added "temporarily" still inherits every env var including
ANTHROPIC_API_KEY - A permission entry like
Bash(docker *)lets the model rundocker exec -it ... sh - A skill fetched from a community repo may include curl-to-external in its body
- An OAuth
.credentials.jsoncan end up in git history if.gitignoreis misconfigured
Scan monthly. Scan after every plugin/skill install. Scan after upgrading Claude Code — new features shift the threat model.
API keys, tokens, passwords, connection strings — anywhere under ~/.claude/.
~/.claude/settings.jsonand~/.claude/settings.local.json~/.claude.json(MCP server definitions live here, not insettings.json)- Project
.claude/settings.jsonfiles ~/.bashrc/~/.zshrcexports (these leak into every subprocess unless scrubbed — see CC 2.1.98 note below)- MCP server
envblocks - Skills and rules markdown (people paste tokens into examples)
.credentials.json,credentials.json,token.json,*.pem,id_rsa*- Check both working tree AND git history — a token removed from
HEADstill lives ingit log -p
permissions.allow entries that amount to arbitrary code execution:
| Pattern | Risk |
|---|---|
Bash(*) |
Full shell access |
Bash(docker *) |
docker exec -it <container> sh |
Bash(node *) |
node -e "require('child_process').exec(...)" |
Bash(curl *) |
Arbitrary network fetch + pipe-to-shell |
Bash(npx *) |
Arbitrary package execution |
Bash(sudo:*) |
Privilege escalation |
Prefer narrow patterns: Bash(docker ps), Bash(docker logs *), Bash(curl -sf http://localhost:*).
A deny list backstops the allow list. CC 2.1.99 made permissions.deny authoritative — it overrides PreToolUse "ask" hooks, so a correctly written deny entry cannot be bypassed by prompting a hook. But CC 2.1.98 also closed six bypass vectors in the deny matcher itself; your patterns must account for them.
Minimum deny set (every entry should resist CC 2.1.98-era bypass attempts):
"deny": [
"Bash(rm -rf /)*",
"Bash(sudo:*)",
"Bash(chmod 777 *)",
"Bash(> /dev/*)",
"Bash(ssh *)",
"Bash(killall node)*",
"Bash(pkill -f node)*",
"Bash(pkill node)*"
]The closing * and :* variants guard against compound-command smuggling and whitespace padding that earlier CC versions allowed through.
Hooks run unattended. Scan every file in ~/.claude/hooks/ and inline hook commands in settings.json for:
rm -f,rm -rf,> /dev/*,2>/dev/nullmasking errors on deletesgit config --globalmutations- Network egress:
curl,wget,nc,/dev/tcp/,/dev/udp/ - Credential files read:
cat ~/.aws/*,cat ~/.ssh/*,cat **/credentials* - Environment dumps:
env >,printenv >, unscoped$*passed to external commands
MCP definitions live in ~/.claude.json (registered via claude mcp add), not settings.json. The mcpServers key in settings.json is silently ignored — do not rely on it for audit.
Risks to check in ~/.claude.json:
@latesttag on npm MCP servers → supply-chain risk; pin versions- Remote transport (SSE/HTTP) to an unverified endpoint
autoApprove: trueon any tool- Missing
envblock → server inherits every env var (includingANTHROPIC_API_KEY, DB credentials) - Unexpected entries you didn't add (prompt injection via a skill could theoretically suggest
claude mcp add)
Skills and rules are loaded into context unmodified. A malicious or careless body can plant instructions:
- External fetches in SKILL.md (
curl https://...,<img src=http://...>) - Hidden directives ("ignore previous instructions", unicode zero-width text)
allowed-toolsfrontmatter requestingBash(*)- Skills or rules you don't remember adding
Copy-paste greps. Adjust paths as needed.
# 1. Plaintext API keys under ~/.claude/
grep -rEn 'sk-ant-(oat|ort|api)[0-9]{0,2}-[A-Za-z0-9_-]{30,}' <USER_HOME>/.claude/ <USER_HOME>/.claude.json 2>/dev/null
grep -rEn '(api[_-]?key|secret|password|token)["\s:=]+["A-Za-z0-9_-]{20,}' <USER_HOME>/.claude/ 2>/dev/null
# 2. SSH keys / certs accidentally stored
grep -rEn 'BEGIN (RSA|OPENSSH|DSA|EC|PRIVATE) KEY' <USER_HOME>/.claude/ 2>/dev/null
# 3. Broad allow entries
jq '.permissions.allow[]? | select(. | test("\\*\\)$|Bash\\(\\*|Bash\\((docker|node|curl|npx|bash) "))' \
<USER_HOME>/.claude/settings.json
# 4. MCP servers with @latest or missing env
jq '.mcpServers | to_entries[] | select(
(.value.args // [] | any(. | test("@latest$"))) or
(.value.env == null)
) | .key' <USER_HOME>/.claude.json
# 5. Hook scripts doing network egress or unsafe deletes
grep -rEn '(curl|wget|nc|/dev/tcp|/dev/udp|rm -rf|rm -f.*>/dev/null)' <USER_HOME>/.claude/hooks/ 2>/dev/null
# 6. OAuth credential files tracked in git (run in each project)
git ls-files | grep -Ei '(credentials|token|secret).*\.(json|yaml|yml|env)$'
git log --all --diff-filter=A --name-only | grep -Ei 'credentials\.json|token\.json'Two skills do most of the manual work:
/audit-stack— runs the full sweep above (permissions, hooks, MCP, skills, git hygiene) and produces a scored report. Use monthly and after major changes./gitignore-anchor-audit— catches unanchored top-level directory entries in.gitignorethat recursively shadow nested paths (the exact misconfiguration that lets.credentials.jsonsneak into git). Use when adding to.gitignore, after finding a tracked file you didn't expect, or on a new repo.
For public/shared repos, also run a third-party scanner periodically:
npx ecc-agentshield scan <USER_HOME>/.claude/It scores across secrets, permissions, hooks, MCP servers, and skill bodies. Triage anything below 75 this month; anything below 50 this week.
When pushing a config repo (shared-setup, dotfiles, or the guide repo itself):
# Block real token material in staged diff — pattern strings are fine, token bodies are not
git diff --cached | grep -E 'sk-ant-oat01-[A-Za-z0-9_-]{30,}|sk-ant-ort01-[A-Za-z0-9_-]{30,}|sk-ant-api[0-9]{2}-[A-Za-z0-9_-]{30,}' \
&& { echo "REAL TOKEN IN DIFF — STOP"; exit 1; } || echo "no real tokens in diff"
# Block SSH/PEM keys
git diff --cached | grep -E 'BEGIN (RSA|OPENSSH|DSA|EC|PRIVATE) KEY' \
&& { echo "PRIVATE KEY IN DIFF — STOP"; exit 1; } || echo "no private keys"
# Block internal paths if repo is public
git diff --cached | grep -E '/home/[a-z]+/|/Users/[a-z]+/' \
&& echo "WARN: internal path in diff" || echo "no internal paths"Wire the first two into a pre-push hook for any repo that has ever held a credential file.
Several security-relevant changes landed in the 2.1.98–2.1.111 window. Update your mental model:
| Version | Change | What it means for audits |
|---|---|---|
| 2.1.98 | CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 strips Anthropic API creds from subprocesses + triggers PID namespace isolation on Linux |
Set this in .bashrc to stop shell subprocesses from inheriting API keys. Trade-off: incompatible with skipDangerousModePermissionPrompt: true — if you rely on that for automation, the scrub stays off and env-based MCP auth is still exposed. Pick one. |
| 2.1.98 | Six deny-list bypass vectors closed (compound commands, backslash escapes, /dev/tcp, /dev/udp, env-prefix commands, whitespace padding) |
Old deny entries like "Bash(ssh *)" without the closing-paren variant may have been bypassable. Re-audit using the minimum deny set above. |
| 2.1.99 | Settings resilience: unrecognized hook event names fail gracefully instead of nuking the whole settings.json |
A typo in a hook matcher no longer silently disables all your hooks. But a malicious skill that injects a malformed hook now fails quietly — scan hook output regularly. |
| 2.1.99 | permissions.deny overrides PreToolUse "ask" hooks |
Any advice assuming a hook could gate a deny-listed tool is wrong. Deny wins. Move "ask"-style prompts to allowed tools; keep deny for hard blocks. |
| 2.1.105 | PreCompact hook can block compaction via {"decision":"block"} |
Write a PreCompact hook that refuses compaction when context contains secrets-shaped strings or OAuth tokens — prevents a compacted summary from persisting a leaked credential. |
| 2.1.105 | Skill description budget 250 → 1536 chars | Longer descriptions can now hide prompt injection text; include skill descriptions in your grep for hidden directives. |
If you find a leaked credential:
- Rotate immediately. Don't clean up first — rotate the token at the provider (Anthropic console, Google OAuth, etc.) before anything else. Old token is now untrusted.
- Archive the leak. Copy the offending file/commit to an offline archive for audit. Do not force-push the secret out of history until it's rotated — force-push can race with a cloner.
- Scrub history. Once rotated, use
git filter-repoor BFG to purge the file from all refs. Thengit push --force-with-leaseto the public remote. - Audit reach. Check recent provider logs for any access using the leaked token. Treat any unexpected use as hostile.
- Fresh init. Regenerate the Claude Code setup that leaked it — run a clean
~/.claude/tree from a known-good backup repo, then re-apply skills/rules file by file, re-scanning at each step. - Write it up. Add a note to your project memory describing what leaked, how, and what changed. This is the pattern that stops the next one.
- Part I — Installation & Setup: initial permission and hook posture
- Part II — Core Configuration:
settings.jsonstructure, hook catalogue - Part IV — Advanced Workflows: MCP server management
/audit-stackskill — automated monthly scan/gitignore-anchor-auditskill —.gitignorerecursive-shadow detection