Claude Code実践入門の次、claude -p の落とし穴を実測する

目次
  1. claude -p の基本挙動——回答だけが stdout に出る
  2. スクリプトから使うなら –output-format json
  3. ファイル書き込みは権限設定しだいで「黙って失敗」する
  4. –allowedTools で許可する——ただし引数の順番に罠がある
  5. 失敗をスクリプトで検知する——exit code だけを信用しない

「Claude Code実践入門」で検索すると、同名の書籍(『Claude Code実践入門[生成AI深掘りガイド]』、SBクリエイティブ)や『実践Claude Code入門』(技術評論社)のページと書評がずらりと並ぶ。入門書や入門記事で対話モードは一通り使える、という人が次にやりたくなるのは、claude をシェルスクリプトや cron に組み込む自動化だろう。そこで使う非対話モード(claude -p)にはオプションの紹介記事こそあるが、「実際に実行したら何が起きたか」まで載せた記事はほとんど無い。

この記事では、claude -p を初めてスクリプトに組み込むときに私が実際に詰まった点を、手元で実行したログだけを根拠に書く。結論を1つだけ先に言うと、権限拒否で何も作られなかった実行でも exit 0 が返る。知らずに cron へ組み込むと、失敗し続けるジョブが成功に見え続ける。

claude -p の基本挙動——回答だけが stdout に出る

実行環境は次のとおり。

$ bash -c 'claude --version; sw_vers'
2.1.220 (Claude Code)
ProductName:		macOS
ProductVersion:		26.6.1
BuildVersion:		25G76
# exit=0 (410 ms)

ログは検証用のハーネス経由で取っているため、各ログ末尾の # exit= はラップした bash 全体の終了コードを指す。肝心の claude 自体の終了コードは、コマンド直後の echo "exit code: $?" で毎回出力しているので、そちらを見てほしい。

-p--print)を付けると、Claude Code は対話画面を開かず、1回だけ応答して終了する。

$ bash -c 'cd /tmp/claude/claude-p-demo && claude -p "1+1の答えだけを数字で出力して"; echo "exit code: $?"'
2
exit code: 0
# exit=0 (5038 ms)

数秒で返り、stdout には回答テキストだけが出る。exit code は 0。変数代入やパイプにそのまま使える形だ。

stdin からの入力も受け取れる。ゼロ除算を仕込んだ小さなファイルを用意して、パイプで流し込んでみる。

def average(numbers):
    return sum(numbers) / len(numbers)


if __name__ == "__main__":
    print(average([]))
$ bash -c 'cd /tmp/claude/claude-p-demo && cat buggy.py | claude -p "このPythonコードのバグを1行で指摘して"; echo "exit code: $?"'
`average([])` は `len(numbers)` が 0 のため `ZeroDivisionError` で落ちます(空リストの場合の処理が必要)。
exit code: 0
# exit=0 (4669 ms)

仕込んだバグを一発で指摘した。テスト失敗の出力や diff をパイプで渡して所見をもらう、コミットメッセージの下書きを作らせる、といった「読んで返すだけ」の使い方なら、ここまでの知識で動く。詰まるのは、claude に手を動かさせようとしたときだ。

スクリプトから使うなら –output-format json

プレーン出力は人が読むぶんには良いが、スクリプト側で「成功したのか」を判定する材料が無い。--output-format json を付けると、結果がメタデータ付きの JSON になる。

$ bash -c 'cd /tmp/claude/claude-p-demo && claude -p --output-format json "1+1の答えだけを数字で出力して"'
...
# exit=0 (3874 ms)

出力は1行の長い JSON なので、主要フィールドを jq で抜き出したものを載せる(sed -n 2p はログの2行目=JSON本体の切り出し)。

$ bash -c 'sed -n 2p ../logs/011-json-output2.txt | jq "{type, subtype, is_error, num_turns, result, total_cost_usd, session_id, permission_denials}"'
{
  "type": "result",
  "subtype": "success",
  "is_error": false,
  "num_turns": 1,
  "result": "2",
  "total_cost_usd": 0.299102,
  "session_id": "c0c701ca-6cec-4c8c-897e-95359a69659d",
  "permission_denials": []
}
# exit=0 (5 ms)

スクリプトから見るべきは、回答本文の result、エラー有無の is_error、そして次節で効いてくる permission_denials だ。total_cost_usd は API 従量課金に換算したコストで、サブスクリプション利用でも記録される。session_id--resume での再開に使えると公式CLIリファレンスにあるが、今回は確かめていない。逐次出力向けの stream-json という形式もあり、こちらも今回は試していない。

ファイル書き込みは権限設定しだいで「黙って失敗」する

ここからが本題。まず権限系のフラグを何も付けずに、ファイル作成を頼んでみる。

$ bash -c 'mkdir -p /tmp/claude/claude-p-demo && cd /tmp/claude/claude-p-demo && rm -f hello.txt && claude -p "カレントディレクトリに hello.txt というファイルを作り、中身は hello とだけ書いて"; echo "exit code: $?"; ls -la hello.txt 2>&1 || echo "hello.txt is missing"'
hello.txt を作成しました。中身は `hello` の1行です。
exit code: 0
-rw-r--r--@ 1 user  wheel  6 Aug 17 19:48 hello.txt
# exit=0 (7509 ms)

あっさり作られた。だがこれを「claude -p は既定でファイルを書ける」と受け取ると誤る。私のグローバル設定(~/.claude/settings.json)では defaultModeauto になっていて、書き込みが自動承認されていた。

$ bash -c 'jq -r ".permissions.defaultMode" ~/.claude/settings.json'
auto
# exit=0 (6 ms)

つまりこの挙動は自分の settings.json に依存する。インストール直後の素の状態に合わせるため、--permission-mode default を明示して同じ依頼をする。

$ bash -c 'cd /tmp/claude/claude-p-demo && rm -f hello2.txt && claude -p --permission-mode default --output-format json "カレントディレクトリに hello2.txt というファイルを作り、中身は hello とだけ書いて"; echo "exit code: $?"; ls -la hello2.txt 2>&1 || echo "hello2.txt is missing"'
...
exit code: 0
ls: hello2.txt: No such file or directory
hello2.txt is missing
# exit=0 (59685 ms)

今度はファイルが作られていない。注目すべきは、それでも exit code は 0 という点だ。何が起きたかは JSON の中に埋まっている。

$ bash -c 'sed -n 2p ../logs/006-perm-default.txt | jq "{is_error, num_turns, duration_ms, denied: [.permission_denials[] | .tool_name]}"'
{
  "is_error": false,
  "num_turns": 9,
  "duration_ms": 60501,
  "denied": [
    "Write",
    "Bash",
    "Bash",
    "Bash",
    "Bash",
    "Write"
  ]
}
# exit=0 (6 ms)

非対話モードには承認プロンプトが無いため、許可されていないツールの実行は拒否される(公式のheadlessモード解説)。ログを見ると、モデルは Write を拒否されたあと Bash 経由の書き込みまで試みて、全部 permission_denials に積まれている。num_turns は 9、duration_ms は 60501。つまり60501ミリ秒も別ルートを試し続けた挙句に何も作れず、それでも is_error は false、プロセスは正常終了する。cron に仕込んだスクリプトがこの状態に陥ると、exit code を見ている限り「毎晩成功している」ように見え続ける。

–allowedTools で許可する——ただし引数の順番に罠がある

素の権限のまま書き込みを通すには --allowedTools "Write" を付ける。ここでも一度詰まった。プロンプトをオプションの後ろに置いたら、こう落ちた。

$ bash -c 'cd /tmp/claude/claude-p-demo && rm -f hello3.txt && claude -p --permission-mode default --allowedTools "Write" "カレントディレクトリに hello3.txt というファイルを作り、中身は hello とだけ書いて"; echo "exit code: $?"; cat hello3.txt 2>&1 || echo "hello3.txt is missing"'
exit code: 1
cat: hello3.txt: No such file or directory
hello3.txt is missing
Error: Input must be provided either through stdin or as a prompt argument when using --print
# exit=0 (1656 ms)

「プロンプトが渡っていない」というエラーで exit 1。--allowedTools はツール名を空白区切りで複数受け取るオプションなので、直後に置いたプロンプトまでツール名の続きとして解釈されたものと見られる。プロンプトを -p の直後に移すと通る。

$ bash -c 'cd /tmp/claude/claude-p-demo && rm -f hello3.txt && claude -p "カレントディレクトリに hello3.txt というファイルを作り、中身は hello とだけ書いて" --permission-mode default --allowedTools "Write"; echo "exit code: $?"; cat hello3.txt 2>&1 || echo "hello3.txt is missing"'
hello3.txt を作成し、中身に `hello` と書き込みました。
exit code: 0
hello
# exit=0 (6575 ms)

今度はファイルが実際に作られ、中身も指示どおりになっている。スクリプトに書くときは「プロンプトを先、オプションを後」に統一しておくのが安全だ。

失敗をスクリプトで検知する——exit code だけを信用しない

CLI としての引数エラーは exit code で拾える。

$ bash -c 'claude --nonexistent-option 2>&1; echo "exit code: $?"'
error: unknown option '--nonexistent-option'
exit code: 1
# exit=0 (106 ms)

だが前々節で見たとおり、権限拒否で何もできなかった実行は exit 0 で終わる。だから claude -p を組み込むスクリプトでは、確認を三段構えにする必要がある。まず exit code(CLI レベルの失敗を拾う)、次に --output-format jsonis_errorpermission_denials が空であること(実行はされたが作業が拒否されたケースを拾う)、最後にファイル生成や変更を頼んだのなら成果物そのものの存在(モデルが「できた」と言っただけのケースを拾う)。今回のログでも、権限拒否の痕跡は exit code にも stdout の見た目にも出ておらず、permission_denials の中にしか残っていなかった。

最後に、この使い方が向かないケースを書いておく。

  • 低レイテンシ用途。 単純な質問でも応答に数秒かかり、権限で迷走した回は60501ミリ秒かかった。エディタの補完のような用途には向かない
  • 出力の文面に依存する処理。 同じ buggy.py を同じプロンプトで2回レビューさせたら、指摘内容は同じでも文面は毎回違った(verify のログ2本で確認した)。パースしたいなら文面ではなく JSON の構造化フィールド側に寄せる
  • コストを見ずに回すループ。 実測では1回の単純な質問が total_cost_usd 換算で 0.299102 だった。サブスクリプション枠でも消費はするので、大量に回す前に1回あたりの記録を見ておく

なお、この実測はすべて claude 2.1.220 / macOS 26.6.1 でのもので、権限まわりの挙動は自分の settings.json の内容に依存する。別バージョン・別設定では同じ結果になるとは限らない。入門の次の一歩で決めるべきは「claude に何をさせるか」より先に「失敗をどう検知するか」だった、というのが今回の結論だ。