0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の PreToolUse フックで git add -A や reset --hard を機械的に止める(誤検知の避け方つき)

0
Last updated at Posted at 2026-10-31

何に困ったか

複数の Claude Code セッションを、同じ1つのクローンで同時に動かしています。
CLAUDE.md には「git add -A と reset --hard と stash は使わない」と書いていました。
ただ、文章での「お願い」は、守られないことがあります。他のセッションが書きかけのファイルを git add -A で一緒にコミットしてしまう、といったぶつかりが起きました。

そこで、書くだけでなく、フックで機械的に止めることにしました。
この記事では、実際に使っているフック(止めたときの文章だけ短くしています)と、そのテスト、残っているつまずきを書きます。

環境

  • Claude Code(CLI)、Python 3(手元の確認は 3.11)
  • フックは ~/.claude/hooks/git-guard.py に置き、~/.claude/settings.json に登録

設定: settings.json

Bash ツールを呼ぶ前(PreToolUse)に、スクリプトを動かします。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/git-guard.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

フックには、標準入力で JSON が渡されます。コマンドの文字列は tool_input.command に入っています。
終了コード 2 で終わると、そのツール呼び出しは止められ、標準エラーに書いた文章が Claude に返ります。

フック本体: git-guard.py

#!/usr/bin/env python3
import json
import re
import sys

data = json.load(sys.stdin)
cmd = data.get('tool_input', {}).get('command', '') or ''

# コミットメッセージなど、引用符の中の文字は調べない
body = re.sub(r"'[^']*'", "''", cmd)
body = re.sub(r'"(?:\\.|[^"\\])*"', '""', body)
body = re.sub(r'<<\s*[\'"]?(\w+)[\'"]?.*?^\1$', '', body, flags=re.S | re.M)

END = r'(?=\s|$|;|&|\||\))'
RULES = [
    (r'\bgit\s+add\s+(?:\S+\s+)*?(?:-A|--all)' + END, 'git add -A / --all'),
    (r'\bgit\s+add\s+(?:\S+\s+)*?\.' + END, 'git add .'),
    (r'\bgit\s+commit\s+(?:\S+\s+)*?(?:-a\w*|--all)' + END, 'git commit -a'),
    (r'\bgit\s+reset\s+(?:\S+\s+)*?--hard' + END, 'git reset --hard'),
    (r'\bgit\s+checkout\s+(?:--\s+)?\.' + END, 'git checkout -- .'),
    (r'\bgit\s+restore\s+(?:\S+\s+)*?\.' + END, 'git restore .'),
    (r'\bgit\s+stash\b', 'git stash'),
    (r'\bgit\s+push\s+(?:\S+\s+)*?(?:-f|--force|--force-with-lease)(?:=\S*)?' + END, 'git push --force'),
    (r'\bgit\s+clean\s+(?:\S+\s+)*?-\w*f', 'git clean -f'),
]

for pat, name in RULES:
    if re.search(pat, body):
        sys.stderr.write(
            f'止めました: {name}\n'
            '共用クローンです。ほかのセッションの書きかけを消したり混ぜたりしないよう、この操作はしません。'
            'add は自分が変えたファイルを名指しで、してください。\n'
        )
        sys.exit(2)
sys.exit(0)

ポイントは、最初の3行の re.sub です。
git commit -m "git add -A は禁止" のように、引用符の中に書いただけの文字では止まらないようにするため、引用符の中身を空にしてから調べています(ヒアドキュメントも同様)。
これを入れる前は、コミットメッセージに git add -A と書いただけで止まるつまずきがありました。

止めたときの文章には、「どうすればよいか」(ファイルを名指しで add する)も入れています。Claude がそれを読んで、回り道をせずにやり直せるようにするためです。

動作の確認

実際に、フックへ JSON を流して確かめました(リポジトリの控えのファイルで実行)。

t() {
  printf '{"tool_input":{"command":%s}}' \
    "$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$1")" \
    | python3 config/claude/hooks/git-guard.py >/dev/null 2>/tmp/e
  echo "exit=$? : $1 : $(head -1 /tmp/e)"
}
t 'git add -A'
t 'git add a.md b.md'
t 'git commit -m "git add -A は禁止"'
t 'git reset --hard HEAD'
t 'git push --force origin main'
t 'git push origin HEAD:main'
t 'git stash'
t 'git commit -a -m x'

結果は次のとおりです。

  • git add -A → exit=2(止まる)
  • git add a.md b.md → exit=0(通る)
  • git commit -m "git add -A は禁止" → exit=0(引用符の中なので通る)
  • git reset --hard HEAD → exit=2
  • git push --force origin main → exit=2
  • git push origin HEAD:main → exit=0
  • git stash → exit=2
  • git commit -a -m x → exit=2

つまずき: 同じ行の別コマンドを誤検知する

正規表現を (?:\S+\s+)*? で緩くつないでいるため、; や && でつないだ別のコマンドの引数まで拾います。

t 'git push origin main; pkill -f foo'    # exit=2 : git push --force と誤検知
t 'git add a.md; ls .'                    # exit=2 : git add . と誤検知
t 'git add a.md && echo -A'               # exit=2 : git add -A と誤検知

pkill -f の -f が、git push の -f(force)に見えてしまいます。
止めすぎる分には安全側なので、私は直さずに受け入れました。止まったらコマンドを分けて打ちます。

  • git push origin main と pkill -f foo を別々の呼び出しにする
  • git add a.md と ls . を別々の呼び出しにする

誤検知を減らしたい場合は、;・&&・| でコマンドを分割してから、それぞれに RULES を当てる形にすると直せるはずです(私はここまでは試していません)。

今回やらなかったこと

  • フックは「止める」だけで、書き換えはしません
  • git switch や git rebase のような他の危ない操作は、今のルールには入れていません
  • Windows での動作は確認していません(上の確認は Linux と macOS のシェルです)

まとめ

  • ルールを CLAUDE.md に書くだけでは守られないことがある。守らせたいことは PreToolUse フックで止める
  • 終了コード 2 と標準エラーの文章で、Claude に「止めた理由」と「代わりの手」を返せる
  • 引用符の中を空にしてから調べると、コミットメッセージでの誤検知を避けられる
  • 同じ行の別コマンドを誤検知する癖は残る。コマンドを分けて打つ

追記(2026-11-01): git -C などを挟むとすり抜けていた

コメントでご指摘をいただき、手元で確かめました。上の RULES は git の直後にサブコマンドが来る前提なので、次の3つは止まらずに通っていました。

  • git -C sub add -A
  • git -c core.x=1 add -A
  • git -C . push --force

git とサブコマンドの間の全体オプションを飛ばして見る部品を足し、各ルールの先頭の \bgit\s+ をこれに置き換えました。

# git と サブコマンドの間の全体オプション(-C <dir>、-c <k=v>、--git-dir=… など)も飛ばして見る
GIT = r'\bgit(?:\s+(?:-[Cc]\s+\S+|--(?:git-dir|work-tree|namespace|exec-path|config-env)(?:=\S+|\s+\S+)|--[\w-]+(?:=\S+)?|-[pP]))*\s+'
RULES = [
    (GIT + r'add\s+(?:\S+\s+)*?(?:-A|--all)' + END, 'git add -A / --all'),
    # ほかの行も同じように GIT + r'サブコマンド…' にする
]

直したあとの確認です(2 = 止めた、0 = 通した)。

2  git -C sub add -A
2  git -c core.x=1 add -A
2  git -C . push --force
2  git --git-dir=.git --work-tree=. reset --hard
2  git -C ~/workspace stash
0  git -C ~/workspace add projects/a.md
0  git -C x commit -q -m "add -A"
0  git -C ~/workspace log --all --oneline

もう1つ、「;・&&・| で分割してから当てる」と書いた部分です。Claude Code の権限ルール(permissions.deny)は、すでにこの分割をしていました。公式ドキュメント(Configure permissions の Bash rules の節)によると、deny と ask のルールは、&&・;・| などで連結した各サブコマンドにも照合されます。サブシェルやコマンド置換 $() の中も対象です。
一方で、同じ節には「Bash(git push *) は git -C . push origin main を止めない」とも書かれています。連結は権限ルールで、書き方の揺れはフックで止める、という分け方になります。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?