cronからclaude -pを動かす:隔離、JSONスキーマ、すぐ止める
161回の定期実行と671回のヘッドレスClaude Code呼び出しから学んだこと。隔離するフラグ、スキーマで検証する出力、そしてリトライしても直らない失敗。
Claude Codeの -p(print)モードを使うと、対話型のコーディングエージェントをスクリプトから呼べるコマンドにできます。私たちは定期ジョブで使っています。4つの生成タスクを1日3回実行し、それぞれ日本語・韓国語・英語の短い構造化テキストを作り、検証してMySQLのテーブルに書き込みます。2026年9月17日から11月8日までに、ジョブは161回実行され、モデルを671回呼び出しました。
この記事では、コードから claude -p を安全に呼ぶために学んだことを書きます。プロンプトではなく配管の話です。CLIを周囲から隔離すること、構造化された出力を得ること、どの失敗をリトライするか決めることです。
呼び出し
以下のフラグは、すべて Claude Code 2.1.274 の --help に載っています。
cmd = [
claude_bin, "-p", user_message,
"--model", model,
"--effort", effort,
"--system-prompt", system_prompt,
"--json-schema", json.dumps(schema, ensure_ascii=False),
"--output-format", "json",
"--tools", "", # ツールなし:テキストを入れてJSONを出す
"--no-session-persistence", # 実行間で何も保存しない
"--setting-sources", "", # ユーザー・プロジェクトの設定を無視
"--strict-mcp-config", # ここで渡さない限りMCPサーバーなし
"--disable-slash-commands",
]
with tempfile.TemporaryDirectory(prefix="gen_") as cwd: # CLAUDE.md を拾わない
proc = subprocess.run(cmd, cwd=cwd, env=minimal_env(), capture_output=True,
text=True, timeout=600)
周囲から隔離する
対話型のエージェントは、周りの文脈を拾うように作られています。プロジェクトの CLAUDE.md、ユーザー設定、設定済みのMCPサーバー、実行してよいツール。バッチジョブでは、そのどれもが「昨日の無関係な変更が今日の出力を変える」経路になります。
- 空の作業ディレクトリ。 呼び出しごとに新しい一時ディレクトリを使えば、スケジューラーがどこから起動しても、プロジェクトの指示は読まれません。
- ツールなし、設定なし、MCPなし。 モデルの仕事はJSONを返すことだけです。間違ってもコマンドを実行したり、サーバーを呼んだりできないようにします。
- セッションを残さない。 呼び出しは毎回独立しているので、おかしな出力が次の呼び出しに持ち越されません。
- 最小限の環境変数。 渡すのは
HOME、USER、LOGNAME、TMPDIR、LANG、LC_ALL、SHELLと固定のPATHだけです。別のClaude Codeセッションの中や、APIの認証情報をexportしたシェルからジョブが起動すると、それらの変数でCLIが使う認証情報や設定が変わることがあります。きれいな環境から始めれば、この種の驚きはまとめてなくなります。
構造化出力:まずスキーマ、そして疑う
--json-schema と --output-format json を使うと、結果のオブジェクトの structured_output がスキーマに従います。スキーマはコードで組み立て、厳しくしています。
- 値の集合がわかっている所はすべて
enum。次の週末の2つの対象日のように、計算で決まる値も含みます - すべての階層で
additionalProperties: false - 個数が重要な配列は、長さを固定
そのうえで、書き込む前にすべてのルールをPythonでもう一度確認します。個数、範囲、項目内や項目間の重複、空でないテキスト。言語のチェックもあり、日本語と英語の欄にハングル、韓国語と英語の欄にかなが入っていれば正規表現で弾きます。スキーマで保証されるのはパースできることで、正しいことではありません。失敗した試行のうち4回は、スキーマには合っているのに日本語の欄に韓国語が混ざったJSONでした。検証で弾き、リトライで成功しています。
結果のJSONに含まれるメタデータ(使ったモデル、入出力トークン、所要時間)もログに残します。以下の数字はそこから取りました。
どの失敗をリトライするか
各タスクは3回まで試行します。161回の実行で、失敗した試行は54回でした。
| 原因 | 失敗した試行 | リトライは効く? |
|---|---|---|
| CLIの認証情報の期限切れ | 48 | 効かない。 人がもう一度サインインするまで、毎回同じように失敗する |
| 検証で出力を拒否(言語の混在) | 4 | 効く。次の試行で通った |
| タイムアウト(600秒) | 2 | 効く |
教訓は1行目です。CLIに保存されたサインインは、2026年10月6日と11月4日、4週間おきに2回期限切れになりました。そのたびに、予定していた2つの枠で4つのタスクすべてが失敗し、失敗した枠ごとに無駄な呼び出しが12回(4タスク×3回)発生しました。人は失敗の通知で気づき、サインインし直して、取りこぼした枠を手で実行し直しました。
対策は、リトライのループを飛ばし、残りのタスクも中止する専用の例外です。
class AuthExpiredError(GenerationError):
"""認証情報の期限切れ。誰かがサインインし直すまで、リトライしても成功しない。"""
if result.get("is_error") or result.get("subtype") != "success":
detail = str(result.get("result"))[:500]
if "authenticate" in detail or "OAuth" in detail:
raise AuthExpiredError(detail)
raise GenerationError(detail)
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
return validate(call_claude(...))
except AuthExpiredError:
raise # 残りの試行やタスクを無駄にしない
except (GenerationError, ValueError, KeyError) as e:
log.warning("attempt %d failed: %s", attempt, e)
通知には、やるべきことをそのまま書いています。サインインして、--force で実行し直す、です。
罠がもう1つあります。認証情報がすでに切れているのに、CLIのステータスコマンドは「ログイン済み」と答えていました。そのためヘルスチェック(--check)では、ステータスを聞くのではなく、ごく小さな生成を実際に1回行います。本番と同じ経路を通らないチェックは、チェック自体が動くことしか証明しません。
スケジュールまわり
- 同時に1つだけ。
fcntlのロックファイルを使います。実行には時間がかかる(4タスクで中央値約6.5分、最大約25.6分)ので、重なりは実際に起こりえます。 - 枠ごとに冪等。 小さな状態ファイルに、タスクごとの最後に終わった枠を記録します。強制しない限り、再実行は終わった仕事を飛ばします。だから「サインインを直して再実行」はいつでも安全です。
- launchdにはローカルのログパスを。 macOSでは、launchdが外付けボリューム上のログファイルを開けず、ジョブが終了コード78で起動しませんでした。launchd自身のstdout/stderrは
~/Library/Logs/に置き、アプリケーションのログはどこに置いても構いません。
数字
| 項目 | 値 |
|---|---|
| 定期実行 | 161回(完了157、一部失敗4:すべて認証の期限切れ) |
| 記録したモデル呼び出し | 671回 |
| 1回あたりの所要時間 | 中央値約68秒(2〜593秒) |
| 1回あたりの出力 | 中央値約4.1kトークン(最大26.7k) |
| 全体の実行(4タスク) | 中央値約6.5分、最大約25.6分 |
チェックリスト
- [ ] 空の一時ディレクトリで、
--tools ""、--setting-sources ""、--strict-mcp-config、--disable-slash-commands、--no-session-persistenceを付けて実行する - [ ] 最小限で明示的な環境変数を渡す
- [ ]
--json-schema+--output-format json。structured_outputを読み、すべてのルールをコードで再検証する - [ ] 認証エラーは実行全体の致命的エラーとして扱い、直し方をそのまま通知する
- [ ] ヘルスチェックは実際に呼び出す
- [ ] 重なりをロックで防ぎ、枠を冪等にし、結果のトークン数と所要時間を記録する
よくある質問
スクリプトから使うとき、claude -p がプロジェクトのファイルやツール、MCPサーバーを読まないようにするには?
空の一時ディレクトリで実行し、--tools ""、--setting-sources ""、--strict-mcp-config、--disable-slash-commands、--no-session-persistence を付け、環境変数も最小限にします。入力はシステムプロンプトとメッセージだけになります。
claude -p から確実にJSONを得るには?
厳しいスキーマ(enum、additionalProperties: false)を --json-schema で渡し、--output-format json の結果から structured_output を読みます。そのうえで、書き込む前にすべてのルールを自分のコードで検証します。
リトライしてはいけない失敗は?
CLIの認証情報の期限切れです。私たちのログでは、失敗した54回の試行のうち48回が、同じ認証エラーを無駄にリトライしたものでした。検知したら実行全体を止め、人に知らせます。