DEV Community

Evan Cates
Evan Cates

Posted on • Originally published at ludoonus.github.io

Stop AI Coding Agents From Leaking Secrets (.env, API keys, tokens)

An AI coding agent with shell access will, eventually, try to commit something it shouldn't. Not maliciously — it runs git add -A to "save progress," and an untracked .env goes in with everything else. The commit message says chore: sync. The next git push sends your database password to a remote you can't fully un-send it from.

Why this happens specifically with agents

Humans develop a reflex: you glance at git status before staging, and an unexpected .env jumps out. Agents don't have that reflex unless you give it to them. They pattern-match "stage the work" to git add -A or git add ., both of which sweep up every untracked file in the tree — secrets, local configs, key material, and (in repos with worktrees) gitlinks to other sessions.

Why "just add it to .gitignore" isn't enough

.gitignore only protects files that were never tracked. The dangerous cases slip through anyway: a .env.local variant not in your ignore patterns, a key pasted into a normally-tracked config file, or a secret in a file the agent created this session. You need a check on the content of the outgoing commits, not just filenames.

The fix: scan outgoing commits before every push

The right place to catch a secret is the moment before it leaves your machine — at git push, against the commits you're actually about to send (@{u}..HEAD), not the whole history and not just the working tree.

upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)
range=${upstream:+$upstream..HEAD}

# names: catch .env and key material in outgoing commits
git diff --name-only "$range" | grep -qE '(^|/)(\.env|.*\.pem|id_rsa|id_ed25519)$' \
  && { echo "sensitive file in outgoing commits"; exit 2; }

# content: catch credential-shaped strings in the added lines
patterns='(AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|sk-[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----)'
git diff "$range" | grep -qE "^\+.*$patterns" \
  && { echo "credential-shaped string in outgoing diff"; exit 2; }
Enter fullscreen mode Exit fullscreen mode

Wire that into a Claude Code PreToolUse hook matched on git push, and use gitleaks as the primary scanner with the regex layer as a portable fallback. The hook exits 2 to block, and the agent gets told why — so it stops instead of retrying.

If a secret already pushed

Treat it as compromised the instant it reaches a remote, even a private one. Rotate the credential first; rewriting history (git filter-repo) is cleanup, not containment. This is exactly why a pre-push gate is worth the five minutes to set up — recovery is always more expensive.


I packaged this as a one-command Claude Code plugin — a pre-push secret scanner (gitleaks + regex fallback + forbidden-file check), plus dangerous-command and worktree gates. Free and MIT-licensed: cc-powerpack on GitHub.


These practices are covered in depth in The Claude Code Operator's Handbook — 18 chapters on running AI coding agents safely and efficiently. Read a free sample or get it ($29).

Top comments (0)