用 PreToolUse Hook 拦截 Bash 里的 git 命令,防止 Claude 直接提交到 main、乱写 commit message、执行危险操作。
Claude Code 有权限直接跑 git commit、git push、git checkout。大多数时候这很方便,但也意味着它可能:
Hooks 可以在这些操作发生前介入——拦截、校验、强制符合规范,或者直接阻止危险操作。
git 操作在 Claude Code 里都是通过 Bash 工具执行的。所以拦截点是:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [...]
}
]
}
}
Hook 通过 stdin 拿到完整的命令字符串,在里面检查是否包含特定的 git 操作,然后决定放行还是拦截。
基本模式:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# 检查是否是 git commit
if echo "$cmd" | grep -qE '^git commit'; then
# 做检查...
echo "不符合规范" >&2
exit 2 # 阻断并告知 Claude
fi
最常见的需求。Claude 应该在 feature 分支上工作,不应该直接改 main。
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# 只处理 git commit
echo "$cmd" | grep -qE '^git (commit|push)' || exit 0
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ "$current_branch" == "main" || "$current_branch" == "master" ]]; then
echo "禁止在 $current_branch 分支上直接提交。请先切换到 feature 分支。" >&2
exit 2
fi
Claude 收到这个提示后,会主动切换到合适的分支再继续。
很多团队用约定式提交(Conventional Commits):feat: xxx、fix: xxx、docs: xxx。但 Claude 写的 message 格式不一定符合你的规范。
用 Hook 在提交前校验:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
echo "$cmd" | grep -qE '^git commit' || exit 0
# 提取 -m 后面的 message
msg=$(echo "$cmd" | grep -oP '(?<=-m )["\x27].*?["\x27]' | tr -d '"'"'" || true)
if [[ -z "$msg" ]]; then
exit 0 # 没有 -m,可能是用编辑器,跳过
fi
# 检查是否符合 Conventional Commits 格式
if ! echo "$msg" | grep -qE '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{3,}'; then
echo "Commit message 格式不符合规范。" >&2
echo "正确格式:<type>(<scope>): <description>" >&2
echo "示例:feat(auth): add OAuth login" >&2
echo "可用 type:feat | fix | docs | style | refactor | test | chore | perf | ci | build | revert" >&2
exit 2
fi
Claude 收到反馈后会重新构造符合格式的 commit message。
你想在每次 Claude 提交时自动带上署名,记录是 AI 参与的提交。但不想每次手写。
这个用 PostToolUse 更合适——在 commit 完成后检查是否包含署名,没有的话提示 Claude 用 --amend 补上:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# 只处理 git commit(排除 --amend 避免循环)
echo "$cmd" | grep -qE '^git commit' || exit 0
echo "$cmd" | grep -q 'amend' && exit 0
# 检查最新 commit 是否有 Co-Authored-By
last_msg=$(git log -1 --format="%B" 2>/dev/null)
if ! echo "$last_msg" | grep -q 'Co-Authored-By'; then
echo "提交缺少 Co-Authored-By 署名。" >&2
echo "请运行:git commit --amend -m \"\$(git log -1 --format='%s')\" --trailer 'Co-Authored-By: Claude Sonnet 4.6 <[email protected]>'" >&2
exit 2
fi
有些命令一旦执行就很难恢复:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# 阻止 force push 到主分支
if echo "$cmd" | grep -qE 'git push.*--force(-with-lease)?.*\b(main|master)\b'; then
echo "禁止 force push 到 main/master 分支。" >&2
exit 2
fi
# 阻止 reset --hard(需要显式确认)
if echo "$cmd" | grep -qE 'git reset --hard'; then
echo "git reset --hard 是破坏性操作,已被拦截。如果确实需要,请在命令前加 # ALLOW: 注释说明原因。" >&2
exit 2
fi
# 阻止删除远端分支
if echo "$cmd" | grep -qE 'git push.*--delete|git push.*:'; then
echo "禁止删除远端分支。如需删除,请手动在 GitHub 上操作。" >&2
exit 2
fi
第二条规则里有个小技巧:要求 Claude 在命令里加 # ALLOW: 注释来"解锁"操作,并说明原因。这样既有保护,又不会完全死锁。
记录 Claude 执行过的所有 git 操作,方便回溯:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
echo "$cmd" | grep -q '^git' || exit 0
echo "$(date '+%Y-%m-%d %H:%M:%S') GIT: $cmd" >> ~/.claude/git-audit.log
这个用 PostToolUse 放在检查之后,只记录实际执行的命令。
把上面的规则整合进一个脚本:
.claude/hooks/git-guard.sh:
#!/bin/bash
set -e
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
[[ -z "$cmd" ]] && exit 0
echo "$cmd" | grep -q '^git' || exit 0
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
# 1. 禁止在 main/master 上 commit 或 push
if echo "$cmd" | grep -qE '^git (commit|push)'; then
if [[ "$current_branch" == "main" || "$current_branch" == "master" ]]; then
echo "当前在 $current_branch 分支,禁止直接提交或推送。请切换到 feature 分支。" >&2
exit 2
fi
fi
# 2. commit message 格式校验
if echo "$cmd" | grep -qE '^git commit.*-m'; then
msg=$(echo "$cmd" | grep -oP '(?<=-m )["\x27][^\x27"]*["\x27]' | tr -d '"'"'" || true)
if [[ -n "$msg" ]] && ! echo "$msg" | grep -qE '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{3,}'; then
echo "Commit message 格式不符合规范。" >&2
echo "格式:<type>(<scope>): <description>" >&2
echo "示例:fix(api): handle null response from upstream" >&2
exit 2
fi
fi
# 3. 禁止危险操作(除非有 ALLOW 注释)
if echo "$cmd" | grep -qE 'git push.*--force|git reset --hard|git push.*--delete'; then
if ! echo "$cmd" | grep -q '# ALLOW:'; then
echo "危险操作已被拦截:$cmd" >&2
echo "如需执行,在命令末尾加上 # ALLOW: <原因>" >&2
exit 2
fi
fi
settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/git-guard.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'cmd=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .command); echo \"$cmd\" | grep -q \"^git\" && echo \"$(date +\\'%Y-%m-%d %H:%M:%S\\') $cmd\" >> ~/.claude/git-audit.log || true'"
}
]
}
]
}
}
matcher 要用 Bash,不是 Git
Claude Code 没有专门的 Git 工具,git 命令都通过 Bash 执行。所以 matcher 必须是 "Bash",不能是 "Git"。
命令解析不要过度依赖正则
git 命令的写法很多:git commit -m "msg"、git commit --message="msg"、GIT_AUTHOR_NAME=xxx git commit 等。正则写太死会漏掉边缘情况。建议只拦截最常见的危险模式,其他的相信 Claude 的判断。
全局 vs 项目级
保护 main 分支、格式校验这类规则建议放项目级(.claude/settings.json),因为不同项目的主分支名、commit 格式可能不同。审计日志建议放全局,统一记到 ~/.claude/git-audit.log。
用 Hooks 管理 git 工作流的关键在于:PreToolUse + Bash matcher + git 命令检测。
三个最值得配的规则:
1. 禁止在 main 分支直接提交
2. 校验 commit message 格式
3. 拦截 force push、reset --hard 等危险操作
配合审计日志,Claude 的每一次 git 操作都可追溯。规则从简单的开始,跑通一个再加下一个。