
🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 185 篇,AI 编程最佳实战「2026」系列第 65 篇
大家好,欢迎来到 术哥无界 | ShugeX | 运维有术。
我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者!
Talk is cheap, let's explore。无界探索,有术而行。
让 Agent 在真实仓库里写代码,有一个问题大多数教程都跳过了:Agent 并不天然理解哪些 git 命令是危险的。它只会按最大似然执行你交代的事。于是你常常看到这些场面:
git push --force,把同事还没合并的提交覆盖了git commit -m "done",其实 typecheck 压根没跑过这三个场景,发生在三个不同的时间点:命令执行前、commit 之前、冲突出现时。Matt Pocock 的 skills 仓库里,正好有三样东西分别守着这三个点:git-guardrails-claude-code(预防)、setup-pre-commit(提交)、resolving-merge-conflicts(冲撞)。
这篇文章不打算逐个介绍这三个 skill,那是产品文档干的事。我想用一条主线把它们串起来:三层护栏,各自拦住一类 Git 事故。

假设你带着 A、B、C 三个同事在同一个 TypeScript 项目里干活,每人配了一个 Claude Code 风格的 Agent。
三个人的 Agent,分别可能制造三类事故。
A 的 Agent 在 push 时发现远端有别人推的新提交,它不停下来问,而是直接 git push --force,把还没被 push 的远端提交覆盖了。
B 的 Agent 改完类型错误就直接 commit,其实改动让另外两个文件 typecheck 爆了,没人跑过,代码进了仓库。
C 的 Agent 在 rebase 冲突时看到 <<<<<<< 标记,选了看起来比较新的那边,可需求要的是另一边的逻辑。
这三类事故的共同点:都发生在 Agent 对 git 的行为失控时。而且每一类,都对应一个可以拦截的时间点:
命令执行前 commit 前 冲突出现时
│ │ │
git push --force commit 脏代码 rebase 挑错一边
│ │ │
[预防层] [提交层] [冲撞层]
git-guardrails setup-pre-commit resolving-merge-conflicts下面按时间顺序,一层一层看它们各自拦住了什么。

先回到 A 的事故。git push --force 这种命令,能拦的窗口只有一个:命令真正执行之前。
Claude Code 在这个位置有一个专门机制:PreToolUse hook。它在 Claude 决定调用 Bash 工具之后、工具执行之前触发,标准输入会收到一段 JSON,里面带着 Claude 准备执行的完整命令。
Matt 的 git-guardrails-claude-code 就守在这个位置。核心脚本 block-dangerous-git.sh 逻辑很简单:
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
DANGEROUS_PATTERNS=( "git push" "git reset --hard" "git clean -f" "git clean -fd" "git branch -D" "git checkout \." "git restore \." "push --force" "reset --hard" )
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$pattern"; then
echo "BLOCKED: '$COMMAND' matches dangerous pattern '$pattern'. The user has prevented you from doing this." >&2
exit 2
fi
done
exit 0从 stdin 读 JSON,用 jq 取出 tool_input.command,正则匹配危险模式。命中就向 stderr 打一条消息,然后 exit 2。除了带 git 前缀的命令,它还会拦下独立模式的 push --force、reset --hard。
为什么偏偏是 exit 2?因为 Claude Code 的 hooks 机制里,只有 exit 2 才是真正的阻止信号。官方文档说得直白:exit 0 是成功,但不代表批准(staying silent doesn't approve it);exit 1 是非阻塞错误,动作照常执行,官方原话是 If your hook is meant to enforce a policy, use exit 2.。换句话说,网上那些用 exit 1 拦截危险命令的示例,基本形同虚设。
被拦下之后,Agent 看到的是这样一条消息:
BLOCKED: 'git push origin main' matches dangerous pattern 'git push'.
The user has prevented you from doing this.Agent 会明白自己没权限,然后停下来。这正是执行前拦截的意义所在。如果等到 CI 才拦,命令已经跑完了。CI 在 push 之后才触发,拦不住远端被覆盖,更拦不住本地 git reset --hard 丢掉一整天的工作。
反例一:把 guardrails 装在 CI 而不是 agent hook 上。 这个思路听起来合理:反正 CI 会检查。但 CI 的检查发生在 push 之后,而 git push --force 覆盖远端、git reset --hard 丢本地改动,都发生在 CI 之前。等 CI 跑完,事故已经完成了。PreToolUse 之所以装在这里,是因为它是能在命令执行前这个时间点说话的机制。
安装时有一个选择:项目级还是全局。项目级写进仓库的 .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-git.sh"
}
]
}
]
}
}全局则写进 ~/.claude/settings.json,hook 路径换成 ~/.claude/hooks/block-dangerous-git.sh。skill 安装时会问你要哪一种;如果 settings 已存在,会把新 hook 合并进已有的 hooks.PreToolUse 数组,不覆盖其他配置。
这里有两个边界必须交代清楚。第一,这层只管 Claude Code。PreToolUse 是 Claude Code 的工具权限机制,别的 Agent 工具未必有同样的 hook 接口,换工具就得另想办法。
第二,它是字符串正则匹配,不是命令解析器。git push origin main、git push -f 都会命中 git push 模式,但别指望它能智能判断这条 push 是否安全。它的定位是宁可错杀,不可放过,而且它替代不了 CI 的质量关卡。
装完怎么验证?skill 给了一个很直接的冒烟命令:把 {"tool_input":{"command":"git push origin main"}} 用 echo 管道喂给脚本,正确结果应该是 exit 2,并向 stderr 打印 BLOCKED。这个验证步骤很重要,因为它同时确认了脚本路径、jq 依赖和退出码三件事。

再回来看 B 的事故。Agent 改完代码直接 commit,没跑过 typecheck。这个时间点(commit 之前)对应的护栏是 setup-pre-commit。
它给仓库装上 Husky pre-commit hook,编排顺序是固定的。.husky/pre-commit 文件内容如下(Husky v9+ 不需要 shebang):
npx lint-staged
npm run typecheck
npm run test配套的 lint-staged 配置:
{
"*": "prettier --ignore-unknown --write"
}为什么 lint-staged 要排在全量 typecheck / test 前面?因为 lint-staged 只处理暂存文件,也就是这次 commit 真正会带走的文件。lint-staged 官方的理由就两条:全量跑太慢,而且全量 lint 对无关文件会产生无关结果,原话是 Ultimately you only want to check files that will be committed. 所以先用毫秒级的 Prettier 把暂存文件收拾干净,再跑全量的 typecheck 和 test。
反过来把全量测试放前面,中型仓库一次提交可能就要跑几分钟,人很快就想用 git commit --no-verify 绕过去。
反例二:pre-commit 里跑全量测试,而不是 lint-staged。 这会让每次提交都变慢,慢到让人想绕过它。lint-staged 的价值恰恰是快:只检查即将被提交的文件。
还有几个编排细节,都是社区踩过坑的结论:
tsc --noEmit 是项目级全量语义检查;lint-staged 会把暂存文件列表追加到命令末尾,tsc 就变成单文件模式,结果不可靠。所以这里把 npm run typecheck 作为独立一行全量跑。git add,写了反而画蛇添足。typecheck / test 脚本时,跳过对应行,而不是补一个空脚本。补一个 echo "no tests" 之类的空脚本,等于制造虚假的安全感,每次提交都通过但什么都没检查。没有测试就如实说,把质量闸门交给 CI。indent、quotes 这类格式化规则,两边会打架,用 eslint-config-prettier 一次性关掉冲突规则(社区统计大约 30 多条)。这里有个坑:eslintrc 的 extends 管不到你在 rules 里显式写的规则,得用 npx eslint-config-prettier <file> 检测出来删掉。装上之后,第一次提交本身就是冒烟测试:skill 会让你提交一条 Add pre-commit hooks (husky + lint-staged + prettier),这次提交会真的触发新装的 hook。
回到 B 的案例。Agent 改完类型错误,准备 git commit。pre-commit 先跑 lint-staged 格式化暂存文件,然后全量 typecheck 直接爆了。Agent 看到失败,回去修,再 commit,再跑,直到 typecheck 通过。这就是提交层的拦截点。
也要交代清楚边界:pre-commit 不是安全边界,只是纪律工具。git commit --no-verify 能绕过它,CI 才是最终关卡。它的价值是把没跑过的代码挡在 commit 之前,而不是替代 CI 做质量把关。
补充一个上下文:在 Matt 的主流程 skill implement 里,commit 之前本来就有 typecheck、单测、全量测试和 code-review 关卡。pre-commit 是兜底的一道机械保险,而不是全部防线。提交层护栏做得再好,也不该指望它替代 Agent 提交前的自查。

接下来是 C 的事故:rebase 冲突。这一层是 resolving-merge-conflicts,它和前两层不一样:前两层装好之后自动生效,这一层是事件驱动的,冲突出现时才调用。它的 SKILL.md 只有 14 行,流程 5 步:
--continue,直到所有 commit 走完反例三:merge 冲突只看代码不看上下文。 这是新手 Agent 常见的行为:看到冲突标记,比较两边代码谁看起来对,选一个。GitKraken 的指南里有一句话点得很透:冲突标记告诉你改了什么,但没告诉你为什么改。第 2 步强制先找 primary source,就是为了让你按意图解决,而不是按看起来的样子解决。
回到 C 的案例。Agent 在 rebase 时遇到冲突,两个分支都改了同一个函数。它准备选看起来比较新的那边,但第 2 步拦住了它:读了两边的 commit message 才发现,feature 分支的改动是需求方明确要的新逻辑,main 分支的改动只是重构时的顺手调整。
按意图,应该保留 feature 的逻辑,再把 main 的重构部分合进去。这就是保留双方意图。
第 4 步同样关键。冲突解决后必须跑自动化检查。GitKraken 把 Not Testing After Resolution 列为常见错误,理由很硬:合并的组合可能产生双方单独都没有的新 bug。代码看着对,一跑 test 就露馅。
这里有四个边界,务必记牢:
--continue 之后就不能 abort 了。GitLab 文档明确:一旦执行 git rebase --continue,rebase 就不能再回滚。想 abort 要在继续之前决定。git checkout --ours|--theirs 会整体覆盖整个文件,丢掉文件里另一侧的非冲突修改。只想解决单个 hunk,得用 git merge-file。这也是为什么流程要求逐 hunk,而不是整文件二选一。--abort 是 Matt 的个人哲学,不是社区共识。GitHub 和 GitLab 官方都把 --abort 当成合法逃生通道,社区主流观点是:abort 会丢弃已做的全部解决工作,所以应该在深入解决之前判断要不要放弃,而不是解决到一半才反悔。
三个时间点、三个 skill、三类事故,对应关系是:
时间点 | skill | 拦住的事故 | 安装方式 |
|---|---|---|---|
命令执行前 |
| force push 覆盖、reset --hard 丢工作 | 一次性安装 |
commit 前 |
| 提交没跑过 typecheck 的代码 | 一次性安装 |
冲突出现时 |
| rebase / merge 冲突挑错一边 | 按需调用 |
这里有一个分类事实值得留意:resolving-merge-conflicts 在 Matt 的 skills/engineering 主列表里,是日常推广的;git-guardrails-claude-code 和 setup-pre-commit 放在 skills/misc,作者自己标注为 保留但很少用、未在插件中推广(Tools I keep around but rarely use — not promoted in the plugin)。这个分类透露了作者的优先级:冲突解决是他工作流里常遇到的事,所以放主列表;guardrails 和 pre-commit 是备着偶尔用的。这不代表后者没用,只代表作者的使用频率。
安装顺序上,我的建议是分三天装,而不是一次装齐:
git-guardrails-claude-code。它是三层里能拦危险命令的那一层,几分钟装完,收益立竿见影。setup-pre-commit。lint-staged 和 typecheck 开始兜住没跑过就提交的代码。resolving-merge-conflicts。它是事件驱动的,没有冲突时装了也学不会。为什么不是一次装齐?因为前两层安装都有人工确认步骤:guardrails 要你选项目级还是全局、要自定义拦截清单;pre-commit 要检测包管理器、要确认 typecheck / test 脚本是否存在。
一次装齐,你会对每个配置项都无感确认,到头来根本不知道装了什么、拦什么。分开装,每装一层都清楚它防什么。
还要区分两类性质:Claude Code 专属和仓库通用。git-guardrails-claude-code 靠的是 Agent 工具的 PreToolUse hook 能力,只对 Claude Code 生效,换一个 Agent 工具就得另想办法;setup-pre-commit 和 resolving-merge-conflicts 是仓库级的,装进仓库后,任何参与 git 工作流的人都要遵守,不管是你的同事还是任何一个 Agent。
回头看那三类事故:force push 覆盖远端、commit 没跑过的代码、rebase 冲突挑错一边。它们不在同一个时间点发生,所以也不该靠同一种手段解决。三层护栏的边界,恰恰就是 Agent 失控的三种时间窗口。
再说句老实话:这三层都不是铜墙铁壁。hook 是字符串正则匹配,--no-verify 可以绕过 pre-commit,按意图解决冲突也不能排除逻辑错误。
它们的价值不是消灭所有事故,而是把事故从悄无声息地发生,变成在每个时间点都被迫停下来一次:危险命令推不出去,脏代码进不了仓库,冲突得先想清楚再继续。对一个要让 Agent 在真实仓库里干活的团队,这已经是投入产出比划算的三层保险。
说明:本文内容基于 Matt Pocock skills 仓库源码(mattpocock/skills)与 Claude Code 官方 hooks 文档分析整理而成,文中涉及的拦截脚本、hook 配置和安装步骤以本地仓库版本为准,尚未在生产环境中逐项验证。文中的配置模板和参数建议仅供参考,实际效果请以你的业务数据和环境测试结果为准。 如果你也遇到过 Agent 弄坏仓库的场面,欢迎在评论区分享你的踩坑和护栏方案。
好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。