Claude Media
Claude Codeでforce pushを制限する — --force-with-leaseだけ許可する権限

Claude Codeでforce pushを制限する — --force-with-leaseだけ許可する権限

Claude Codeにgit push --forceを打たせず、--force-with-leaseだけ通す権限ルールの書き方です。deny優先の評価順、ルールの照合の癖、フックとGitHubの保護ブランチとの分担まで説明します。

Claude Codeに履歴の書き換えを許すなら、git push --forceは禁止し、git push --force-with-leaseだけを通すのが現実的な線引きです。ただしBash(git push *)を拒否するだけでは、この2つを区別できません。denyはallowより先に評価されるため、拒否ルールの中に許可の例外を作れないからです。

必要なのは、--forceと-fの形だけを個別に拒否し、--force-with-leaseの形だけを許可するルールの組み立てです。この記事では、そのsettings.jsonの書き方と、ルールがすり抜ける形、フックによる補強、GitHubの保護ブランチとの分担まで順に説明します。

まず置く権限設定

結論の設定を先に示します。プロジェクトの.claude/settings.jsonに入れる形です。

{
  "permissions": {
    "allow": [
      "Bash(git push --force-with-lease *)",
      "Bash(git push --force-with-lease=* *)"
    ],
    "deny": [
      "Bash(git push --force *)",
      "Bash(git push * --force)",
      "Bash(git push * --force *)",
      "Bash(git push -f *)",
      "Bash(git push * -f)",
      "Bash(git push * -f *)"
    ]
  }
}

役割は次のとおりです。

  • denyの6本は、--forceと-fが先頭・中間・末尾のどこに置かれても拾います
  • allowの2本は、--force-with-leaseをgit pushの直後に置いた形だけを通します
  • --force-with-leaseをリモート名やブランチ名の後ろに置いた形は、どちらにも当たらず通常の確認に回ります

最後の点は欠点ではなく意図した設計です。許可する書式をひとつに絞ると、Claudeが別の並びで書いたときに人が確認できます。CLAUDE.mdに「force pushはgit push --force-with-lease origin <branch>の順で書く」と書いておけば、Claudeは許可される形を選びやすくなります。

なぜ「denyの中に例外」は作れないのか

評価順はdeny、ask、allowで、最初に当たったものが結果を決めます。ルールの具体性は順序を変えません。広いBash(aws *)のdenyは、より狭いBash(aws s3 ls)のallowがあっても全件ブロックします。

このため、次の書き方は意図どおりに動きません。

前と後

denyで広く止めてallowで例外を作る書き方は通らない

BEFORE

動かない形

denyにBash(git push *)、allowにBash(git push --force-with-lease *)を置く。denyが先に当たり、--force-with-leaseも止まる。

AFTER

動く形

denyは--forceと-fを含む形だけに絞る。allowは--force-with-leaseの形だけを書く。両者の文字列が重ならないので、順序に依存しない。

同じ理由で、askにBash(git push *)を置くのも避けます。askはallowより先に評価されるため、--force-with-leaseを許可しても毎回確認が出ます。通常のpushに確認を残したいなら、askは使わず、広いallowを書かない形にします。allowに当たらないpushは、既定の確認に回ります。

ルールの照合で外れやすい3か所

Bashルールは、コマンド文字列に対して*を任意のテキストとして照合します。force pushのルールで、特に外れやすいのは次の3か所です。

ワイルドカードの前の空白もルールの一部

Bash(git push --force *)はgit push --force origin mainに当たります。一方でgit push --force-with-lease origin mainには当たりません。--forceの直後に空白が必要だからです。これが「--forceだけ拒否して--force-with-leaseは残す」ことを成り立たせています。

ルール末尾の*の直前の空白は、ルールの一部として扱われます。Bash(ls *)はlsofに当たらず、Bash(ls*)は当たる、というのと同じ仕組みです。逆に、末尾の*は引数なしのコマンドにも当たるので、git push -f単体もBash(git push -f *)で拾えます。

中間のワイルドカードは「何も無い」に当たらない

Bash(git push * --force)はgit push origin main --forceに当たりますが、git push --forceには当たりません。*の前後の空白が両方必要で、git log * mainがgit log mainに当たらないのと同じ挙動だからです。先頭に置く形を別ルールで拾っているのは、そのためです。

書き方を変えると当たらない

Bashルールはコマンド文字列そのものを照合します。同じプログラムを別の形で呼ぶと、denyルールはすり抜けます。ドキュメントはBash(git push *)について、次の形が止まらない例として挙げています。

書き方Bash(git push *)で止まるか
git push origin mainBash(git push *)で止まるか止まる
git -C . push origin mainBash(git push *)で止まるか止まらない
git -c push.default=current push origin mainBash(git push *)で止まるか止まらない
git 'push' origin mainBash(git push *)で止まるか止まらない

force pushのdenyも同じ事情を抱えます。さらにgit側には、git push origin +mainのようにrefspecの先頭に+を付けて--forceと同じ効果を出す書き方があります。これも上の6本には当たりません。ルールの本数を増やして追いかけるより、次節のフックで文字列全体を検査する方が漏れにくくなります。

一方、timeout 30 git push --forceのようにラッパーを付けた形は、ラッパーを外してから照合されます。FOO=bar git push --forceのような環境変数の代入付きも、denyは代入を越えて当たります。git add . && git push --forceのように複合コマンドの一部になっていても、サブコマンド単位でdenyが適用されます。

フックで補強する

ルールが苦手な「文字列全体を見て判断する」処理は、PreToolUseフックに任せられます。フックは権限ルールを迂回できず、denyとaskは常に評価されます。フックとルールは足し算で使えます。

次のスクリプトは、gitとpushを含み、かつ--force・-f・+refspecのいずれかが単語として現れるコマンドを拒否します。--force-with-leaseは単語が一致しないので通ります。

#!/bin/bash
# .claude/hooks/block-force-push.sh
COMMAND=$(jq -r '.tool_input.command')
 
if echo "$COMMAND" | grep -Eq 'git .*push' \
  && echo "$COMMAND" | grep -Eq '(^| )(-f|--force|\+[A-Za-z0-9_./-]+)( |$)'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "force pushは禁止です。--force-with-lease origin <branch> を使ってください"
    }
  }'
else
  exit 0
fi

chmod +xで実行権限を付け、settings.jsonに登録します。コマンドは${CLAUDE_PROJECT_DIR}で始めます。相対パスのままだと、Claudeがサブディレクトリへ移動した後にスクリプトが見つからず、フックが働きません。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-force-push.sh" }
        ]
      }
    ]
  }
}

スクリプトはjqに依存します。判定は正規表現による近似で、gitとpushの間にコマンドを挟んだ誤検知や、引用符の中の-fの取りこぼしは起こりえます。拒否の理由文にClaudeが取るべき書式を入れておくと、次の試行で許可される形に直しやすくなります。フックの書き方全般はフックの解説にまとめています。

--force-with-leaseにも穴がある

--force-with-leaseは万能の安全弁ではありません。gitのドキュメントは、期待値を明示しない形(--force-with-lease単独、または--force-with-lease=<ref>)は、バックグラウンドでgit fetchを走らせる処理と相性が悪いと説明しています。fetchがリモート追跡ブランチを更新すると、リースは「他人の変更を見たうえで上書きしてよい」状態になり、保護が実質的に消えるためです。

念のため期待値まで固定するなら、期待するコミットまで指定する--force-with-lease=<ref>:<expect>を使います。ドキュメントによれば、この形だけが期待値を明示した形で、ほかの形は実験的な扱いです。--force-if-includesを併用して、取り込んでいないリモートの更新があれば強制更新を拒否する方法もあります。

先の設定のBash(git push --force-with-lease=* *)は、この明示形を通すための行です。許可する範囲を明示形だけに絞りたい場合は、Bash(git push --force-with-lease *)の行を外します。--forceと--force-with-leaseを同時に指定されると、--forceが--force-with-leaseのチェックを無効にするため、denyが必ず勝つ形になっていることも重要です。

保護ブランチとの分担

権限ルールもフックも、Claude Codeを動かすマシン上でコマンドを止める仕組みです。リモートへの強制更新そのものを止めるのは、GitHubなどサーバー側の仕事です。

GitHubの保護ブランチのルールは、既定で対象ブランチへのforce pushとブランチ削除を無効にします。ただし既定では、リポジトリの管理者権限や「保護を迂回する」権限を持つ人にはルールが適用されません。制限を管理者にも及ぼす設定が別にあります。Claude Codeを管理者のトークンで動かしているなら、この設定を確認する価値があります。

層止められるもの止められないもの
denyルール止められるものClaudeが書く典型的なforce push止められないものgit -Cなど別の書き方、人が打つコマンド
PreToolUseフック止められるもの文字列全体の検査で拾える形止められないもの正規表現が拾えない書き方、Claude以外の経路
保護ブランチ止められるもの保護対象ブランチへの強制更新(管理者を除く既定)止められないもの保護されていないブランチ、迂回権限を持つ主体

Claudeに自分のfeatureブランチだけforce pushさせたいなら、保護ブランチはmainやreleaseに限定し、feature側はClaude Code側の--force-with-leaseだけ許可する構成になります。mainを守るのはサーバー、feature側の事故を減らすのは権限ルール、という分担です。

シェルのコマンドそのものをOSレベルで制約したい場合は、サンドボックスという手段もあります。ただしサンドボックスは主にファイルとネットワークの制限で、gitの引数の中身を見分ける仕組みではありません。

どの設定ファイルに書くか

denyは、どの設定の層に置いても他の層のallowに勝ちます。ユーザー設定のallowとプロジェクト設定のdenyが食い違えば、denyが優先されます。組織の管理設定に置いたdenyは、コマンドライン引数でも上書きできません。

この性質から、置き場所は目的で分けられます。

  • 個人の習慣として止めたいなら、ユーザー設定にdenyを置く。どのリポジトリでも効く
  • チームで揃えたいなら、プロジェクトの.claude/settings.jsonに置いてリポジトリに入れる
  • 例外なく止めたいなら、管理設定に置く。ユーザー側のallowで抜け道を作れない

--force-with-leaseのallowはプロジェクト側に置く方が安全です。個人の設定に広いallowが紛れ込むと、チームのdenyの意図とずれた確認なしの経路が生まれやすくなります。ただしdenyは常に勝つため、allowの置き場所を間違えても、force pushが通るわけではありません。

手元で動作を確かめる

実際のリモートに触れずに確かめるには、ローカルのbareリポジトリを相手にします。

git init --bare /tmp/remote.git
git clone /tmp/remote.git /tmp/work && cd /tmp/work
git commit --allow-empty -m "first" && git push origin HEAD
git commit --amend -m "rewritten"
mkdir -p /tmp/work/.claude
cat > /tmp/work/.claude/settings.json <<'EOF'
{
  "permissions": {
    "allow": [
      "Bash(git push --force-with-lease *)",
      "Bash(git push --force-with-lease=* *)"
    ],
    "deny": [
      "Bash(git push --force *)",
      "Bash(git push * --force)",
      "Bash(git push * --force *)",
      "Bash(git push -f *)",
      "Bash(git push * -f)",
      "Bash(git push * -f *)"
    ]
  }
}
EOF

/tmp/workに置いたsettings.jsonは、冒頭の権限設定と同じ内容です。フックも試すなら、スクリプトを/tmp/work/.claude/hooks/に置き、登録の設定も足します。

この状態でClaude Codeを/tmp/workで起動します。初回はフォルダーを信頼するダイアログが出ます。受け入れる前はallowが効かないので、手順3の「確認なしで通る」は信頼を受け入れた後の挙動です。起動したら、「履歴を書き換えたのでpushして」と頼みます。--forceで打とうとすればdenyで止まり、--force-with-lease origin <branch>ならそのまま通ります。止まったときの拒否メッセージは、/permissionsのルール一覧と見比べると、どのルールが当たったか追えます。

試すのは次の3つです。

  1. git push --force origin HEADがdenyで止まる
  2. git push origin HEAD --force-with-leaseが、どちらにも当たらず確認に回る
  3. git push --force-with-lease origin HEADが確認なしで通る

2番目の挙動を確かめておくと、「許可していない書式は確認に落ちるだけで、拒否まではされない」ことが実感できます。

確認とトラブルの切り分け

設定を入れたら、/permissionsでルールの出どころが想定どおりか確認します。ルールは追加した次のツール呼び出しから効きます。

push自体が想定外にブロックされるときは、force pushの設定とは別の問題の可能性があります。auto modeでは、許可したはずのpushが分類器に止められる報告が出ています。詳しくはauto modeでpushがブロックされる件を参照してください。

.claude/settings.jsonに書いたallowルールは、フォルダーを信頼するダイアログを受け入れた後にだけ適用されます。denyとaskは制限するだけなので、信頼の前でも効きます。つまり、チームでリポジトリに設定を置く場合、denyはクローンした直後から効き、--force-with-leaseの許可は各自が信頼を受け入れてから効くことになります。

同じ型の「特定の書き込み系コマンドだけ確認に残す」設計は、DVCのdvc pushやSalesforce CLIの本番deployでも使っています。コマンドの名前を変えれば、考え方はそのまま流用できます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/hooks
  3. github.com/git/git/blob/master/Documentation/git-push.adoc
  4. docs.github.com/en/repositories/configuring-branches-and-merges-in-your-re…
この記事を共有:XはてブLinkedIn