免費

用 Hooks 管理 git 工作流:讓 Claude 提交代碼也守規矩

用 PreToolUse Hook 攔截 Bash 裡的 git 命令,防止 Claude 直接提交到 main、亂寫 commit message、執行危險操作。


Claude Code 有權限直接跑 git commitgit pushgit checkout。大多數時候這很方便,但也意味著它可能:

  • 提交到 main 分支
  • 寫出格式隨意的 commit message
  • 漏掉 co-author 署名
  • 在不該的時候 force push

Hooks 可以在這些操作發生前介入——攔截、校驗、強制符合規範,或者直接阻止危險操作。


思路:用 PreToolUse 攔截 Bash 裡的 git 命令

git 操作在 Claude Code 裡都是透過 Bash 工具執行的。所以攔截點是:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [...]
      }
    ]
  }
}

Hook 透過 stdin 拿到完整的命令字串,在裡面檢查是否包含特定的 git 操作,然後決定放行還是攔截。

基本模式:

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

if echo "$cmd" | grep -qE '^git commit'; then
  echo "不符合規範" >&2
  exit 2
fi

實戰一:禁止直接提交到 main

最常見的需求。Claude 應該在 feature 分支上工作,不應該直接改 main。

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

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

實戰二:強制 commit message 格式

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

echo "$cmd" | grep -qE '^git commit' || exit 0

msg=$(echo "$cmd" | grep -oP '(?<=-m )["\x27].*?["\x27]' | tr -d '"'"'" || true)

[[ -z "$msg" ]] && exit 0

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

實戰三:自動補充 Co-Authored-By

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

echo "$cmd" | grep -qE '^git commit' || exit 0
echo "$cmd" | grep -q 'amend' && exit 0

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

實戰四:阻止危險的 git 操作

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

if echo "$cmd" | grep -qE 'git push.*--force(-with-lease)?.*\b(main|master)\b'; then
  echo "禁止 force push 到 main/master 分支。" >&2
  exit 2
fi

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

實戰五: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

組合配置:完整的 git 工作流 Hook

.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")

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

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
    exit 2
  fi
fi

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

幾個注意事項

matcher 要用 Bash,不是 Git

Claude Code 沒有專門的 Git 工具,git 命令都透過 Bash 執行,所以 matcher 必須是 "Bash"

命令解析不要過度依賴正規表示式

git 命令寫法很多,正規表示式寫太死會漏掉邊緣情況。建議只攔截最常見的危險模式,其他的相信 Claude 的判斷。

全域 vs 專案級

保護 main 分支、格式校驗建議放專案級.claude/settings.json)。稽核日誌建議放全域,統一記到 ~/.claude/git-audit.log


小結

用 Hooks 管理 git 工作流的關鍵:PreToolUse + Bash matcher + git 命令偵測

三個最值得配的規則:
1. 禁止在 main 分支直接提交
2. 校驗 commit message 格式
3. 攔截 force push、reset --hard 等危險操作

配合稽核日誌,Claude 的每一次 git 操作都可追溯。