Stop Claude Code Push Accidents at the Machine Level: Guarding Against Leaked API Keys and Wrong-Repo Pushes A developer created a machine-level guard for Claude Code to prevent accidental git pushes that leak API keys or commit to wrong repositories. The solution uses a PreToolUse hook that scans staged diffs for secrets and verifies push destination ownership, with an early exit for non-git commands to minimize performance overhead. This is part of my "Claude Code environment" series. Last time I wrote about getting Claude and Codex to collaborate on a single machine https://zenn.dev/bokuwalily/articles/codex-claude-handoff . This time we're at the exit point of that setup: stopping accidents at the machine level when you let an AI drive git . Handing commit and push over to an AI agent is convenient, but two things are scary. 1 It carelessly sweeps up an API key or .env , and PreToolUse hook.Claude Code often has a hook that "scans for secrets when writing a file" for Edit/Write . But that alone leaves a hole. Both git commit and git push go through the Bash tool. You need another gate not at the moment the file is written, but at the moment you stage and push. So I added a PreToolUse hook targeting the Bash tool.This hook fires on every single Bash call . So the top priority is to "spend zero cost on non-git commands." If the string doesn't contain git at the very start, it exits immediately without ever launching Python. 粗い早期return: payload に 'git' が無ければ python を1度も起動せず即終了。 case "$INPUT" in git ;; exit 0 ;; esac Note:It's important to write hooks on the assumption that "every call is taxed." The old implementation launched python3 three times to parse JSON, which taxed every call by several hundred ms. I consolidated parsing into a single pass, and let the vast majority of calls that don't involve git pass straight through via the regex early return. After that, it only enters the main processing when the tool is Bash , the command has a git word boundary, and it contains commit or push . The first thing to block is "disabling the hook" itself. Git commands containing a bypass flag like --no-verify are blocked. if printf '%s' "$CMD" | grep -qE -- ' --no-verify|--no-gpg-sign|commit\.gpgsign=false '; then block "hook bypass フラグが含まれています。明示依頼が無い限り使用禁止。" fi Without this, the agent might learn the worst kind of workaround: "the hook rejected me, so I'll re-run with --no-verify attached." Before the gate itself, ban the act of removing the gate. On git commit , it scans the staged diff git diff --cached . The key is to look only at "the diff that's about to be committed," not the whole working tree. DIFF=$ git diff --cached 2 /dev/null || true AWS Access Key の形 printf '%s' "$DIFF" | grep -qE 'AKIA 0-9A-Z {16}' && block "AWS Access Key が含まれています" secret= "..." / api key: "..." のような代入 printf '%s' "$DIFF" | grep -qE \ ' secret|api key|apikey|access token|private key|client secret :space: := :space: "'"'"' A-Za-z0-9/+ =- {20,}' \ && block "シークレットらしき値が含まれています" .env がステージされている(.env.example はOK) git diff --cached --name-only | grep -E ' ^|/ \.env' | grep -qvE '\. example|sample|template $' \ && block ".env ファイルがステージされています" That third .env check quietly earns its keep. It blocks .env while letting .env.example / .env.sample / .env.template through. It matches the reality that you want to publish the template but must never let the real file out. This is the one that helps me most personally. It verifies that the owner of the push destination repository is mine , and blocks pushes to any owner not on the allowlist. ALLOWED OWNERS="bokuwalily" 自分のGitHub owner(スペース区切りで複数可) ... URL=$ git remote get-url "$REMOTE" OWNER=$ printf '%s' "$URL" | sed -nE 's . github\.com :/ + ^/ + /. \1 p' owner が ALLOWED OWNERS になければ block Why this is needed: when you fork an OSS project and tinker with it, someone else's repository ends up mixed into your remotes. Pushing there by mistake becomes an accident where your code flies off into a third party's repo. I've also renamed my account in the past, so I wanted to mechanically guarantee that "every push to anything other than my current owner gets stopped." When I intentionally push to another owner, I just add it to the allowlist. There are two design principles. decision: block as JSON and exits 2.On top of these two commit scanning + push destination verification , I've separately built a one-command publishing flow that also adds output verification right before push secret → remote → output verification → push → deploy → connectivity check , but the foundation is this hook. git diff --cached .env.example --no-verify PreToolUse gate .env while letting templates throughNext time, I'll write about the pipeline that pours every session's conversation log into long-term memory — Turning Conversation Logs into Obsidian Long-Term Memory https://zenn.dev/bokuwalily/articles/conversation-to-memory . Written by Lily — I ship iOS apps and automate my content stack with Claude Code. Follow along: Portfolio https://bokuwalily.com · X https://x.com/bokuwalily · GitHub https://github.com/bokuwalily