Guides

Keeping secrets safe with Claude Code

Claude Code can read any file in your project, run shell commands and stage commits. That's exactly what makes it useful, and exactly why a stray .env or private key can end up in a diff, a log, or a commit if you haven't set boundaries first.

Where secrets actually leak

None of this requires anything going wrong with the model. It's ordinary agent behavior meeting an ordinary unprotected file.

Deny rules: block the Read tool from opening secrets

Add these to permissions.deny in .claude/settings.json (shared with your team) or ~/.claude/settings.json (just you):

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

Deny rules win over allow rules and can't be overridden by a broader match, so this is a solid first layer. It's worth adding .env.example as an explicit exception if you keep one, since it holds placeholder values you want Claude to see:

"allow": ["Read(./.env.example)"]

The gap: a deny rule on Read only stops Claude's own Read tool. Bash(cat ./.env) still works unless Bash is restricted too, and there's no single Bash pattern that reliably blocks every way to read a file (cat, less, python -c, a here-doc). Treat the deny rule as necessary, not sufficient — pair it with real command inspection. See the permissions guide for the full allow/ask/deny syntax.

A hook that scans staged changes before commit

Claude Code doesn't ship a built-in secret scanner. A PreToolUse hook on Bash can add one: when the command is a git commit, scan the staged diff for common secret shapes and block the commit if it finds one.

#!/usr/bin/env python3
# .claude/hooks/scan-secrets.py -- PreToolUse, matcher "Bash"
import json, re, subprocess, sys

data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")
if "git commit" not in cmd:
    sys.exit(0)

diff = subprocess.run(
    ["git", "diff", "--cached"], capture_output=True, text=True
).stdout

PATTERNS = [
    (r"AKIA[0-9A-Z]{16}", "AWS access key"),
    (r"ghp_[A-Za-z0-9]{36}", "GitHub token"),
    (r"sk_live_[A-Za-z0-9]{16,}", "Stripe live key"),
    (r"-----BEGIN [A-Z ]*PRIVATE KEY-----", "private key"),
]

hits = []
for line in diff.splitlines():
    if not line.startswith("+"):
        continue
    for pattern, label in PATTERNS:
        if re.search(pattern, line):
            hits.append(label)

if hits:
    print(f"Blocked commit: possible {', '.join(sorted(set(hits)))} in staged changes.",
          file=sys.stderr)
    sys.exit(2)

Register it in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [ { "type": "command",
          "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scan-secrets.py" } ] }
    ]
  }
}

Exit code 2 is what blocks the tool call; the text on stderr is what Claude sees as the reason, so keep it descriptive but never echo the matched secret itself. Test the hook directly before trusting it:

echo '{"tool_input":{"command":"git commit -m x"}}' | python3 .claude/hooks/scan-secrets.py; echo "exit=$?"

Widen the pattern list for your own providers, and don't expect regex matching to catch everything — it catches known shapes, not novel ones. See more hook examples, or build and test a rule set without writing the Python yourself using the free hook builder.

Environment variable hygiene

The env key in settings.json sets environment variables for every Claude Code session and the subprocesses it launches:

{
  "env": {
    "NODE_ENV": "development"
  }
}

Use it for switches like this, not for credentials. Anything in env is visible to Claude and, if the file is committed, to everyone with repository access. Keep real secrets in your shell profile, a local .env loaded by your app's own tooling, or a secrets manager, and reference them by name in scripts rather than pasting values anywhere Claude or git can see them.

Checklist

Skip the setup: get the tested versions

Keelwork bundles 10 workflow skills, 5 tested safety hooks (including a full guard-bash and a secret scanner), 3 subagents and 5 CLAUDE.md templates, with a one-command installer that safely merges into your settings.

Get Keelwork — $24 →