Claude Code の auto mode で回している定期処理で、PR のマージが分類器に止められる日が 8 月から断続的に続いていました。
止められたときの理由は「Merge Without Review」です。
マージのコマンドには、allow ルール Bash(gh pr merge *) を置いてありました。
実行記録を洗い直すと、エージェントがマージの前後に確認のコマンドをつないで、1 行で打っていたことが分かりました。
つないだ 1 行は allow ルールでは決まらず、分類器の審査に回っていました。
マージを単独のコマンドで打たせるように手順書を直してから 15 日間、サブエージェントが打った 80 回のマージで、止められた回数は 0 回です。
前提(最小限)
auto mode は、許可ルールに当たらない操作を、分類器と呼ばれる別のモデルが審査して、止めたり通したりする動かし方です。
公式ドキュメントでは、操作は次の順番で判定されると説明されています。
- allow・ask・deny のルールに当たる操作は、その場で決まる
- 読み取りと、作業ディレクトリの中のファイル編集は自動で通る
- それ以外は分類器の審査に回る
ルールに当たった操作は、分類器の判断を待たずに決まります。
私は日次の定期処理を Claude Code のサブエージェントで回していて、自分のサイトのリポジトリに出た PR のレビューとマージまで任せています。
公式ドキュメントによると、サブエージェントの操作も、親のセッションと同じ判定順・同じルールで審査されます。
権限の設定とサンドボックスの違いは、別の記事で整理しました。
許可したはずのマージが止められた
マージを許すルールは、8/17 からリポジトリの .claude/settings.json に置いていました。
実際に使っている設定からの抜粋です。
{
"permissions": {
"allow": [
"Bash(gh pr merge *)",
"PowerShell(gh pr merge *)"
]
},
"hooks": { "...": "省略" }
}
それでも、マージの段で止められる日はなくなりませんでした。
8/25 には、止められたら間を置いて 2 回まで打ち直す手順を足して、しのいでいました。
10/3 には同じ形のコマンドで 3 本をマージしようとして、#683 は通り、#681 と #682 は「Merge Without Review」で止められました。
同じ形なのに結果が分かれるので、分類器が気まぐれに止めているように見えていました。
残っている記録(8/9〜10/3)を後から数えると、サブエージェントがつないだ形で打ったマージは 134 回で、止められたのはそのうち 13 回です。
9 割は通っていたので、当時は一時的な誤判定だと見ていました。
実行記録を洗ったら、コマンドがつながっていた
原因を探すために、サブエージェントの実行記録を読み直しました。
私の環境では、記録は ~/.claude/projects/<プロジェクト>/<セッションID>/subagents/ の下に、エージェントごとの jsonl で残っています。
各行には 1 つのメッセージが入っていて、打ったコマンド(tool_use)とその結果(tool_result)は ID で対応づけられています。
分類器に止められたコマンドだけを抜き出すなら、次の形で足ります。
import json, sys
uses = {}
for line in open(sys.argv[1], encoding="utf-8"):
msg = json.loads(line).get("message") or {}
for b in msg.get("content") or []:
if not isinstance(b, dict):
continue
if b.get("type") == "tool_use" and b.get("name") == "Bash":
uses[b["id"]] = b["input"].get("command", "")
elif b.get("type") == "tool_result" and b.get("tool_use_id") in uses:
text = json.dumps(b.get("content"), ensure_ascii=False)
if "auto mode classifier" in text:
print(uses[b["tool_use_id"]][:120])
10/3 のサブエージェントの記録にかけると、止められた 4 行が出てきました。
そのうち #681 のマージで実際に打たれていたのは、次の 1 行です。
gh pr view 681 --repo <owner>/<repo> --json number,state,mergeable,mergeStateStatus \
&& gh pr merge 681 --repo <owner>/<repo> --merge --delete-branch 2>&1 | tail -2; \
gh pr view 681 --repo <owner>/<repo> --json state,mergedAt
# 実際は 1 行。--jq での整形は省略し、リポジトリ名は伏せた
マージの前に状態を確かめ、マージの出力を tail で短くし、最後にマージ済みかを確かめる流れを、エージェントが 1 行に詰めていました。
#682 も同じ形で、残りの 2 行は待ちのコマンドでした。
gh run watch <id> --exit-status > /dev/null 2>&1; echo "exit=$?"; gh run view <id> …(デプロイの待ち)until gh pr checks … | grep -q …; do sleep 15; done; …(CI の待ちのループ)
一方で、マージより前に打った timeout 900 gh pr checks 685 --watch --interval 30 2>&1 | tail -4 は、つないだ形でも止められていません。
公式の判定順で読むと理由が分かる
公式ドキュメントの Choose a permission mode には、auto mode に入るときに外される allow ルールが挙げてあります。
外されるのは、Bash(*) のように何でも通す広いルールや、Bash(python*) のようなインタープリタのワイルドカードなどです。
Bash(npm test) のような狭いルールは、auto mode でもそのまま効きます。
Bash(gh pr merge *) は外される側に入らないので、ルール自体は生きていました。
効かなかった理由は、つないだコマンドの扱いにあります。
Configure permissions によると、Claude Code は 1 行のコマンドを &&・||・;・|・|&・&・改行で区切り、部分に分けて照らし合わせます。
allow ルールは、分けた部分の 1 つずつに当たる必要があります。
私の 1 行には、tail のように、どの allow ルールにも書いていないコマンドが入っていました。
実は 10/3 の時点で、個人用の設定(.claude/settings.local.json)に Bash(gh pr *) も置いていました。
それでも、tail をはさんだこの 1 行は止められています。
ルールで決まらなかった行は、判定順の 3 番目にあたる分類器の審査に回ります。
分類器が既定で止める操作の一覧には、人が承認していない PR のマージが入っています。
AI の処理に人の承認を挟む考え方は、ヒューマン・イン・ザ・ループの記事で書きました。
公式ドキュメントによると、拒否の理由には多くの場合、分類器が当てはめた規則の名前が入ります。
私の記録では、それが「Merge Without Review」でした。
分類器側の設定の読まれ方は、Configure auto mode に書かれています。
分類器の設定 autoMode が読まれるのは、~/.claude/settings.json・managed settings・--settings の 3 か所です。
プロジェクトの .claude/settings.json と .claude/settings.local.json からは読まれません。
リポジトリに置いた設定ファイルに書いても、分類器の判断は変えられないということです。
また permission modes のページには、会話で許可を出したときの扱いも書かれています。
操作と、その操作を危険にしている具体的な対象を名指しして許可すると、分類器はそれを承認として扱い、止めを解除できます。
定期処理では、人が操作ごとに会話で許可を出すことはないので、これには頼れません。
エージェントに単独で打たせる
直し方として選んだのは、設定を広げることではなく、置いてある allow ルールに当たる形でエージェントに打たせることです。
エージェントが読む手順書は、いまは次の決まりになっています。
- マージは PR ごとに 1 回の呼び出しにし、
&&・;・|・リダイレクトを付けない - 状態の確認は、次の呼び出しで別に打つ
- CI の待ちは
gh pr checks <N> --watch --interval 30を単独で打ち、until … sleep … doneのループは書かない - 止められたら間を置いて同じコマンドを 2 回まで打ち直し、それでも止められたらマージせず、人にコマンドを渡す
- API を直接たたくような、ほかの回避はしない
直した後の形は次のとおりです。
# 1 回目の呼び出し(allow ルール Bash(gh pr merge *) に当たる)
gh pr merge 681 --repo <owner>/<repo> --merge --delete-branch
# 2 回目の呼び出し(状態はここで見る)
gh pr view 681 --repo <owner>/<repo> --json state,mergedAt
私の環境では、gh pr merge は成功しても何も出力しませんでした。
そのため、マージできたかどうかは 2 回目の gh pr view で確かめます。
リダイレクトの 2>&1 や /dev/null への向け先は、公式ドキュメントでは検査の対象外とされています。
それでも手順書では付けない決まりにして、形を 1 つにそろえました。
手順書に理由まで書いたのは、エージェントが確認のコマンドをつなぎたがるからです。
実際、直した後も、マージ後の状態確認を ; や for でつないだ呼び出しが 10/6〜10/15 のうち 8 日で止められています。
拒否の理由は「Merge Without Review」のほか、「Self-Approval」や、説明なしのものもありました。
直した後の 15 日間
手順書を直した翌日の 10/4 は、単独で打ったマージ 8 本がすべて通りました。
10/4〜10/18 の 15 日間では、サブエージェントが単独で打ったマージは 80 回で、止められた回数は 0 回です。
Claude Code の版は 10/3 が 2.1.286 で、10/4〜10/6 も同じ 2.1.286 のまま、その後は 2.1.299 まで上がっています。
ただし、ルールを書いていないほかのコマンドは、その後も同じ理由で止められることがありました。
- 10/5: PR のブランチに最新の main を取り込んでビルドする 1 行(
git merge origin/mainを含む) - 10/9:
dateだけの呼び出し - 10/17:
gh run watch … > /dev/null 2>&1; echo "exit=$?"とつないだデプロイの待ち - 10/18: ローカルで確認用のサーバーを起動する 1 行
どれも理由は「Merge Without Review」でした。
さらに古い記録には、単独の形が止められた例も 1 回だけあります。
8/21 に単独の gh pr merge が 1 回止められ、48 秒後に同じコマンドを打ち直すと通っていました(当時の版は 2.1.234)。
なぜ止められたのかは、記録からは分かりません。
単独で打てば必ず通る、とまでは言い切れないということです。
言えるのは、単独の形にしてから 80 回止められていないことと、ルールを書いていないコマンドをつないだ 1 行はルールの外に出ることの 2 つです。
最後に
allow ルールは、ルールに書いたとおりの形でコマンドが打たれて、はじめて効きます。
AI エージェントは確認や整形のために前後のコマンドをつなぎがちで、ルールを書いていないコマンドが 1 つ混ざれば、その行は許可の外に出ます。
許可ルールを書いたら、エージェントの手順書にも「その操作は単独で打つ」と書いて、形をそろえておくと止まりにくくなります。
止まる理由が気まぐれに見えたら、まず実際に打たれたコマンドの文字列を見てください。
以上です。









コメントを残す