本节摘要:Git Hook 是挂在 Git 事件上的自动化脚本:提交前跑检查、推送前拦强推、接收推送后触发部署。本节先讲 Hook 的位置、激活方式与退出码约定,再分类讲透客户端钩子与服务器端钩子,现场写一个 pre-commit 质量闸门脚本,最后解决"怎么让全团队共用钩子"的协作问题。
阅读完本节,你应当能够:
你的团队提交的代码风格五花八门:有人忘了格式化,有人提交信息写得像"更新",有人把打印语句留在代码里。靠人工 review 拦截?几十个提交根本盯不过来,而且"风格问题"卡住合并又显得吹毛求疵。你想的是:这些检查能不能让工具自动做,连提交的机会都不给不合规代码?
再想一层:你推了代码到远程,希望服务器自动跑测试、自动部署。靠人每次手动触发?漏一次就上线了坏版本。你希望的是:代码一到远程,检查就自动开始。
这两个需求,Git Hook 都能满足。Hook(钩子)是 Git 在执行关键动作前后预留的"挂钩点":钩子上挂了可执行脚本,动作发生时就自动调用。它把"人记得做"变成"系统强制做",把"事后补救"变成"事前拦截"。
类比工厂质检线:产品流经每个工位,工位上都有传感器(Hook)——一个工位检查外观(提交格式),一个工位做功能测试(单元测试),一个工位做包装(部署)。只要任何传感器报警,产品就停在原地,不进下一个流程。Git Hook 就是把质检线装进 Git 操作本身。
每个仓库的 .git/hooks 目录里都躺着十几个示例脚本,以 .sample 结尾:
.git/hooks/ ├── pre-commit.sample ├── commit-msg.sample ├── pre-push.sample ├── pre-receive.sample ├── update.sample ├── post-receive.sample └── ...
激活一个钩子只需两步:把文件名去掉 .sample 后缀,再给它可执行权限(Linux/macOS 用 chmod +x)。示例脚本本身就是注释详尽的模板,直接改就能用。
关键事实:.git/hooks 目录不被版本控制。你克隆一个仓库,不会自动拿到它的钩子——钩子不是仓库内容的一部分。这既是设计(钩子可能依赖本机环境),也是团队协作的痛点(怎么让大家都装上同样的钩子),后文 5.4.3 专门解决。
钩子脚本的退出码决定操作是否继续:
这个约定是 Hook 全部权力的来源。pre-commit 检查到违规,返回 1,提交就被拦下;pre-receive 检查到推送不合规,返回 1,整个推送被拒。写得再好的检查,如果忘了在违规时返回非零,就等于没写——这是写钩子最常犯的低级错误。
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
| pre-commit | commit 生成提交对象之前 | 跑 linter、格式化检查、单测 |
| prepare-commit-msg | 默认提交消息生成后、编辑器打开前 | 按分支名自动注入 issue 编号 |
| commit-msg | 编辑完提交消息后、提交定稿前 | 校验提交消息格式 |
| post-commit | 提交完成后 | 发通知、记录日志 |
| pre-rebase | rebase 开始前 | 拦阻断不该变基的分支 |
| post-rewrite | amend/rebase 等改写完成后 | 同步 issue 状态、通知 |
| pre-push | push 发送引用之前 | 推前跑测试、拦强推历史改写 |
| post-merge | merge 成功后 | 安装新依赖、跑测试 |
前四个(pre-commit、prepare-commit-msg、commit-msg、post-commit)构成"提交四连",覆盖了提交生命周期的每个节点。pre-push 是把关"最后出门"的闸门。

| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
| pre-receive | 服务器收到推送、引用更新前 | 全局策略:拒大文件、拦强推 main、校验提交信息 |
| update | 对每个被推送的引用各执行一次 | 精确到分支/标签的细粒度控制 |
| post-receive | 服务器更新完所有引用后 | 触发 CI 构建、自动部署、发通知 |
pre-receive 是服务器端最强的一道闸——它能基于推送内容、推送者、引用名拒绝整个推送,是团队策略的"最后防线";update 更细,可以单独允许或拒绝某个分支的更新;post-receive 不做拦截,只做事后动作(部署、通知)。服务器端钩子的好处是没法被绕过——客户端钩子用户可以 git commit --no-verify 跳过,服务器端钩子在远端执行,想绕只能不推送。
用 GitHub/GitLab 这类托管平台时,你通常没有直接写 pre-receive 的权限——平台把这类能力封装成了"分支保护规则":禁止直接推 main、要求 PR 通过后才允许合并、要求提交信息含特定前缀、要求有 CI 通过标记等。理解钩子原理后,再看平台这些设置就一目了然:它们本质上是平台托管的 pre-receive 钩子,用图形界面代替了手写脚本。自建 Git 服务器(如 GitLab 自托管)则既可以用分支保护,也可以直接写 pre-receive。两种方式思路一致,只是封装程度不同。
场景:提交前检查暂存区里所有 .txt 文件是否含 "TODO" 字样,有就拦下。完整脚本:
#!/bin/sh # 找出暂存区中所有 .txt 文件 TXT_FILES=$(git diff --cached --name-only -- '*.txt') if [ -z "$TXT_FILES" ]; then exit 0 fi # 逐个检查是否含 TODO for FILE in $TXT_FILES; do if grep -q "TODO" "$FILE"; then echo "错误: 文件 $FILE 包含 TODO,请处理后再提交。" exit 1 fi done exit 0
激活三步:把脚本存成 .git/hooks/pre-commit、去掉后缀、chmod +x 赋可执行权限。之后每次 commit,脚本自动跑——有人提交含 TODO 的文件,提交当场被拦。把 grep -q "TODO" 换成 eslint 或测试命令,就是一个团队级质量闸门。
提交信息质量是团队协作的隐形成本——"update"这种消息三个月后没人看得懂。用 commit-msg 钩子强制格式:
#!/bin/sh # 强制提交信息第一行以 类型冒号 开头,如 feat: 添加登录 MSG=$(head -1 "$1") echo "$MSG" | grep -E "^(feat|fix|docs|refactor|chore):" >/dev/null if [ $? -ne 0 ]; then echo "提交信息必须以 feat 冒号 或 fix 冒号 等开头" exit 1 fi exit 0
这个钩子拿到 Git 传入的提交信息临时文件路径(脚本里的 $1),检查第一行是否符合规范。它比 pre-commit 更"软性"但同样有效——格式问题在提交源头就被拦截,而不是等 review 时被人在 PR 里反复留言"消息规范点"。
钩子"不生效"或"误杀"时,按顺序排查:
ls -l 看一眼。sh .git/hooks/pre-commit 手动执行,看输出和退出码,排错一半问题出在这里。⚠️ 常见坑:Windows 上写钩子脚本最容易栽在换行符上——脚本文件如果保存成 CRLF 换行,sh 解释器可能报错。用 LF 换行保存,或第一行明确 #!/bin/sh。同时记得检查"有没有文件可执行"这个前置条件,很多"钩子不生效"其实是权限问题。
钩子不被版本控制的特性,带来一个协作难题。三种主流解法:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 手动复制 | 文档指导大家拷贝到各自 .git/hooks | 零依赖 | 靠自觉,容易漏装 |
| core.hooksPath | 把钩子放进仓库内目录(如 scripts/hooks),git config core.hooksPath 指向它 | 钩子随仓库版本控制,克隆即得 | 需要每台机器执行一次配置命令 |
| Hook 管理工具 | 用 husky(JavaScript)、pre-commit 框架(Python)等 | 自动安装、多语言、配置共享 | 引入第三方依赖 |
core.hooksPath 是官方推荐方案:Git 2.9 起支持,让 Git 从一个"受版本控制"的目录读钩子,而不是固定读 .git/hooks。团队克隆仓库后只需跑一条配置命令,钩子就位。
git commit --no-verify 可以跳过 pre-commit、commit-msg 等客户端钩子——这是设计的逃生门,但也被想偷懒的人利用。所以真正强制的策略必须放服务器端(pre-receive/update),客户端钩子是"给自觉的人提效率",服务器端钩子才是"给不自觉的人上保险"。
💡 关键直觉:把钩子想成"流程的哨兵"。客户端哨兵守在每个人家门口(防个人手滑),服务器端哨兵守在团队大门(防集体失控)。哨兵要站在合适的位置,才知道该拦什么。
"钩子能用什么语言写?" 任何可执行脚本都行——sh、bash、Python、Ruby 都常见。Git 只要求"是可执行文件 + 遵守退出码约定",不限定语言。小检查用 sh,复杂的用 Python 更可读。
"写坏了一个钩子,会不会把仓库弄坏?" 不会。钩子失败只会中止对应操作(提交、推送),仓库数据和历史不受影响。而且钩子在 .git/hooks 里,删掉文件就等于禁用。
"--no-verify 能跳过所有钩子吗?" 能跳过客户端钩子(pre-commit、commit-msg、pre-push 等),跳不过服务器端钩子——服务器端在远端执行,本地命令管不着。所以强制策略放服务器端才可靠。
"钩子会影响性能吗?" 看脚本复杂度。pre-commit 跑全量 linter 在大仓库可能要几秒,觉得烦就把检查范围缩小到"本次变更的文件"(用 git diff 筛选),而不是全库扫描。
"pre-push 能阻止强推吗?" 能检测但拦不彻底。pre-push 在本地执行,可以检查本次推送是否有非快进更新并中止它——但用户仍能绕过。真正防强推的是服务器端策略(pre-receive 检查引用更新,或平台的分支保护规则)。本地钩子是"提醒",远端钩子是"执法"。
"钩子脚本里的命令找不到怎么办?" 钩子继承的执行环境可能不含你的 PATH(尤其是 GUI 客户端触发的提交)。脚本里尽量写命令的完整路径,或第一行用 #!/bin/sh 并由脚本自身设置 PATH。这也是 Windows 上钩子常"神秘失败"的根源之一。
"pre-commit 里能跑 git 命令吗?" 能,但要小心——钩子运行时仓库处于中间状态。比如 pre-commit 里跑 git status 是安全的,但跑 git commit 会造成递归调用,跑 git rebase 会跟正在进行的提交抢锁。经验法则是:钩子里的 git 命令只读、不改,写入动作留给 Git 主流程去完成。
"服务器端钩子在哪配置?" 在托管仓库的那台服务器上配置,对应仓库的 hooks 目录。GitHub/GitLab 这类托管平台通常不开放直接写 pre-receive,而是提供受管版本(如 GitHub Actions 的 branch protection)——这是平台的封装,思路同源。
下一节给高频命令起"捷径名"——Git 别名,把又长又难记的命令变成顺手的关键词。