「cron に登録した Claude Code のジョブが、朝になっても何も生成していない」「対話では動いたコマンドが、-p を付けた途端に進まなくなる」「ログにエラーが出ていないのに処理が終わっていない」——対話セッションからヘッドレス実行(無人実行)に切り替えた直後によく起きる現象です。
原因の多くは失敗ではなく承認待ちです。対話セッションなら許可を尋ねる表示が出て、人が押せば先へ進みます。しかし claude -p には尋ねる相手がいないため、そこで無音のまま止まります。エラーではないのでログにも残りません。
この記事では、.claude/settings.json の allowlist(許可リスト)だけで無人実行したときに実際に弾かれた3つの書き方と、その回避策を扱います。どれも対話では普通に通る書き方です。
無人実行の「承認待ち」はエラーにならず、無音の停止として現れる
Bash(python3:*) を許可していても、for ループの展開を含む1行は拒否される 許可外のコマンドを
; や && で継ぐと、その1行全体が承認待ちになる 終了コードを取るためにコマンドを足すのではなく、スクリプト側に JSON を出させて読む
公開前の検査に使う正規表現は、自分が書いた注意書きにも当たる
目次を開く
前提:判定はコマンド名ではなく「1行の全体」に効く
.claude/settings.json の permissions.allow に列挙したものだけが、確認なしで実行できます。無人ループ用なら次のくらい絞るのが基本です。
{
"permissions": {
"allow": [
"Bash(python3:*)",
"Bash(mkdir:*)",
"Task"
]
}
} ここで大事なのは、許可の判定が実行しようとしている1行の全体に対して行われることです。python3 が許可されていても、その1行に別の要素が混ざっていれば、行ごと承認待ちになります。以下の3つはすべてここでつまずきます。
落とし穴①:for ループを含む1行は展開の時点で拒否される
複数の引数で同じスクリプトを回したいとき、対話ならこう書きます。
# 無人実行では拒否される
for s in alpha beta gamma; do python3 check.py --exists "$s"; done python3 は許可されているのに、この行は simple_expansion(変数展開)を理由に拒否されます。展開後に何が実行されるかを事前に確定できないためです。
回避策は、1コマンド1引数に開くことです。
python3 check.py --exists alpha
python3 check.py --exists beta
python3 check.py --exists gamma 対象が5件なら5回呼びます。行数は増えますが、無人実行では「短く書く」より1回の実行=許可済みの1コマンドを優先します。件数が多いなら、ループごとスクリプトの内側に入れて python3 check.py --all の形にします。
落とし穴②:; で別のコマンドを継ぐと全体が承認待ちになる
終了コードを確認したくて、つい次のように書きがちです。
# 止まる
python3 check_names.py out.md; echo "rc=$?" これは multiple operations として扱われ、許可リストに入っていない echo が承認を要求します。許可の判定は、1行の中でいちばん弱いところに引きずられます。 && や | でつないだ場合も考え方は変わりません。
書き換えの指針は次の3つです。
| やりたいこと | 止まる書き方 | 通る書き方 |
|---|---|---|
| 実行の成否を知る | python3 check.py x; echo "rc=$?" | スクリプトに {"violations": {}} のような JSON を出力させ、標準出力を読む |
| 前処理をしてから実行 | mkdir -p out && python3 run.py | mkdir -p out と python3 run.py を別々に呼ぶ |
| 複数ステップをまとめる | 長いワンライナー | ステップ全体を1本のスクリプトにして python3 pipeline.py で呼ぶ |
本命は3行目です。シェルで組み立てたくなった処理を、許可済みの実行ファイル1本の内側に押し込むと、無人実行で止まる箇所そのものが無くなります。
落とし穴③:検査用の正規表現は自分の注意書きにも当たる
無人で外部に出す成果物を作るなら、公開前に固有名詞を止めるゲートを挟むことになります。このとき、人名を「漢字1〜4文字+敬称」で拾う正規表現を置くと、敬称で終わる普通の熟語まで人名として検出します(シヨウ・ドウヨウ と読む語などが典型です)。
さらに厄介なのが、その注意書き自体を検査対象のディレクトリに置いてしまうケースです。「この語は誤検出する」というメモがゲートに引っかかり、成果物が公開されなくなります。
- 除外リスト(許可語)を先に用意し、自社名や公開ツール名もそこへ入れる
- 検出したら即停止ではなく、まず一覧をログへ出して人が剪定する
- 運用メモ・教訓ファイルは検査対象のパスから外す
無人化する前のチェックリスト
| 確認すること | 方法 | |
|---|---|---|
| 承認待ちで止まらないか | 本番投入の前に、同じプロンプトを claude -p で1回手動実行して最後まで進むか見る | |
| 1行に複数の操作が混ざっていないか | プロンプトに書いた例示コマンドも対象。; && `\ | for` を含む行を探す |
| 許可が広すぎないか | Bash(*) のような全許可は、無人だからこそ避ける | |
| 翌朝に経緯を読めるか | ログと生成物の出力先を固定し、何が起きたか後から追えるようにする |
なお、拒否の理由として出る文字列や判定の細かさは版によって変わります。手元の版で一度流し、実際のログで確認してから本番の cron に載せてください。
まとめ
無人実行で「何も起きていない」ときは、まずクラッシュではなく承認待ちを疑います。今回の3つはいずれも、python3 という許可済みのコマンドを使っているのに、その周りの書き方で止まっていました。
- ループ展開を含む1行は開いて、1コマンド1引数にする
-
;や&&でコマンドを継がず、まとめたい処理はスクリプトの内側へ入れる - 終了コードではなく、スクリプトが出す JSON で結果を読む
- 公開前の検査は誤検出を前提に、除外リストと人の剪定をセットにする
「対話で通ったから無人でも通る」は成り立ちません。プロンプトに書いた例示コマンドまで含めて1行ずつ見直すのが、結局いちばん早い方法です。
よくある質問(FAQ)
Q. 承認待ちで止まったのか、単に何もしなかったのか、どう見分けますか?
生成物が1つも作られず実行時間だけが長い場合は、承認待ちを疑います。切り分けは同じプロンプトを対話セッションで流すのが確実で、許可を尋ねる表示が出た箇所が、そのまま無人実行で止まる箇所です。
Q. allowlist に Bash(*) を入れれば全部解決しますか?
止まらなくはなりますが、無人実行でこそ避けたい設定です。人が見ていない時間に、想定していないコマンドがそのまま走ります。許可は実行ファイル単位(Bash(python3:*) など)に絞り、足りない分はスクリプト側へ寄せて解決します。
Q. パイプやリダイレクトは使えますか?
; と同じく、1行に複数の操作が並ぶ形は承認対象になりやすい書き方です。出力をファイルに残したいなら、シェルのリダイレクトではなくスクリプトの中で書き出すほうが、無人実行では確実に動きます。
Q. 設定は .claude/settings.json と settings.local.json のどちらに書きますか?
無人ループが依存する許可は、リポジトリで共有する .claude/settings.json に置きます。settings.local.json は個人の一時的な設定用で、共有されないため、別の環境や cron から実行したときに許可が効かず止まる原因になります。
