This is part of my "Claude Code environment" series. Last time I wrote about getting Claude and Codex to collaborate on a single machine. 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.
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 launchedpython3
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)
printf '%s' "$DIFF" | grep -qE 'AKIA[0-9A-Z]{16}' && block "AWS Access Key が含まれています"
printf '%s' "$DIFF" | grep -qE \
'(secret|api_key|apikey|access_token|private_key|client_secret)[[:space:]]*[:=][[:space:]]*["'"'"'][A-Za-z0-9/+_=-]{20,}' \
&& block "シークレットらしき値が含まれています"
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')
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.
*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*