Claude Codeの自動実行における「スケジュール問題」

Claude Codeは、Anthropicが提供するCLIツールであり、claude -p "プロンプト" --permission-mode auto というコマンドひとつでターミナル上からAIにタスクを実行させることができる。当社ではこの仕組みを活用し、毎朝のSEOレポート生成、クラウドワークス案件の定期監視、LINE公式アカウントの資格取得処理など、多岐にわたる業務を自動化してきた。

しかし、自動化が進めば進むほど、ひとつの構造的な問題が顕在化する。それはスケジュール管理の硬直性である。

Windows環境でClaude Codeの定期実行を実現する標準的な方法は、Windowsタスクスケジューラにエントリを登録することだ。schtasks /create コマンドでトリガー時刻、実行頻度、起動するスクリプトを指定する。1つや2つの定期タスクならこれで十分だが、当社では2026年3月時点で22個のスケジュール済みタスクが稼働していた。毎朝3時の学習プロセス、2時間おきのクラウドワークス監視、隔週日曜のLINE資格更新、月次のSEOレポート。それぞれが独立したタスクスケジューラエントリとして存在し、変更のたびに人間が schtasks コマンドを実行しなければならなかった。

この状態の何が問題かを整理すると、3つの摩擦点が浮かび上がる。

第一に、タスクの追加・変更にOS管理者権限が必要であること。Claude Codeはファイルの読み書きやAPIの呼び出しは自律的にできるが、Windowsのタスクスケジューラへの登録はOSレベルの操作であり、人間の介入なしには実行できない。つまり、AIが「毎週火曜15時にこの処理を実行すべきだ」と判断しても、それをスケジュールに反映するには人間がコマンドを打つ必要がある。

第二に、22個のエントリを個別管理する認知コストである。どのタスクが何曜日の何時に動くのか、最終実行はいつだったのか、無効化されているものはどれか。こうした情報がタスクスケジューラのGUIに散在していると、全体像の把握が困難になる。

第三に、隔週実行や月末実行など、タスクスケジューラが標準でサポートしない頻度パターンへの対応が煩雑であること。隔週実行をタスクスケジューラで実現するには「2週間間隔」の設定が必要だが、基準日の管理やずれの補正に手間がかかる。

これらの問題に対する当社の回答が、「スロットディスパッチャー」パターンである。タスクスケジューラには30分ごとに起動する単一のエントリだけを登録し、実際に何を実行するかはJSONファイルに記述する。このJSONファイルはClaude Codeが自由に編集できるため、AIが自律的にスケジュールを管理できるようになる。

スロットディスパッチャーという解法

スロットディスパッチャーの設計思想は、一言で表現すれば「WHENとWHATの分離」である。

従来の構成では、「いつ実行するか(WHEN)」と「何を実行するか(WHAT)」がタスクスケジューラのエントリとして一体化していた。新しい構成では、WHENはタスクスケジューラが担い、WHATはJSONファイルが担う。タスクスケジューラは「30分ごとにディスパッチャーを起動する」という単一の責務だけを持ち、ディスパッチャーが現在時刻に基づいてJSONを参照し、該当するタスクを実行する。

全体のアーキテクチャは次のとおりである。

Windows Task Scheduler(単一エントリ: 30分間隔)
  └─ kuevico-dispatcher.ps1
       ├─ schedule.json を読み込み
       ├─ 現在時刻を30分スロットにスナップ
       ├─ 各タスクの time + freq 条件を評価
       └─ マッチしたタスクを順次実行
            └─ kuevico-run-task.ps1(既存のタスクランナー)
                 └─ claude -p "$(cat prompts/{id}.prompt.md)" --permission-mode auto

この設計がもたらす最大の利点は、Claude Codeが schedule.json を編集するだけでスケジュールの追加・変更・削除を完結できることだ。OS管理者権限は不要で、通常のファイル書き込み権限さえあればよい。タスクスケジューラの登録内容に一切触れることなく、30個でも50個でもタスクを管理できる。

30分という粒度は、1日48スロットを意味する。これは当社の運用では十分な精度であり、分単位の精密なスケジューリングが必要なケースは今のところ存在しない。むしろ30分粒度にすることで、schedule.jsonの time フィールドが "03:00""15:30" のように人間にも読みやすい値になるという副次的な利点がある。

schedule.jsonのスキーマ設計 — 6つの頻度パターンを1つのJSONで表現する

schedule.jsonの設計にあたって最も注力したのは、JSON Schemaとして簡潔でありながら、当社で必要なすべての頻度パターンを表現できるようにすることだった。最終的に定まったスキーマは以下のとおりである。

{
  "defaults": {
    "model": "sonnet",
    "max_budget_usd": 5,
    "timeout_minutes": 60
  },
  "tasks": [
    {
      "id": "morning-learning",
      "name": "毎朝の学習プロセス",
      "time": "03:00",
      "freq": "daily",
      "enabled": true
    },
    {
      "id": "cw-monitoring",
      "name": "クラウドワークス監視",
      "time": ["09:00", "11:00", "13:00", "15:00", "17:00"],
      "freq": "daily",
      "enabled": true,
      "max_budget_usd": 2
    }
  ]
}

defaults オブジェクトはすべてのタスクに適用される既定値を定義する。個別のタスクでこれらのフィールドを指定すれば、既定値を上書きできる。

各タスクの id フィールドは prompts/{id}.prompt.md というファイルパスに直結する。ディスパッチャーはタスクを実行する際、このプロンプトファイルの内容をClaude Codeに渡す。つまり、スケジュールの定義とプロンプトの本体が明確に分離されている。

頻度パターンは freq フィールドと補助フィールドの組み合わせで表現する。当社で実際に使っている6パターンは、daily(毎日)、weekly(毎週)、biweekly(隔週)、monthly(毎月)、monthly + months(四半期等)、そしてinterval(将来拡張用)である。

ディスパッチャーの実装 — 30分スロットマッチングとロックファイル制御

ディスパッチャーの中核ロジックはPowerShellスクリプトに実装されている。最初のステップは、現在時刻の30分スロットへのスナップ処理である。

$now = Get-Date
$slot_minute = [Math]::Floor($now.Minute / 30) * 30
$current_slot = "{0:D2}:{1:D2}" -f $now.Hour, $slot_minute

隔週(biweekly)の判定には、アンカーデート方式を採用した。

$anchor = [DateTime]::Parse($task.anchor)
$diff_days = [Math]::Abs(($today - $anchor).Days)
$is_biweekly_match = ($diff_days % 14 -eq 0)

基準日からの経過日数を14で割った余りが0であれば実行日である。この方式は完全にステートレスであり、過去の実行履歴に依存しない。冪等性が担保されているため、障害でスキップされても次の該当日には正しく実行される。

ロックファイル制御は多重起動を防ぐ安全機構である。前回のディスパッチャーがまだ実行中であれば、新しいインスタンスは即座に終了する。2時間以上経過したロックはstaleとみなして強制解除する。

マッチしたタスクは順次実行される。並列ではなく逐次を選択した理由は、Claude Codeのセッションが同時に複数走るとトークン消費が予測しにくくなるためである。

マルチエージェント環境での安全装置 — 書き込みキーとhookによるアクセス制御

schedule.jsonをClaude Codeが自由に編集できるということは、意図しない編集も起こりうるということだ。当社では複数のClaude Codeセッションが並行して稼働しており、誤ってschedule.jsonを書き換えるリスクがある。

この問題に対して、二重のアクセス制御機構を実装した。

第一の防御層は、Claude Codeのhook機能によるファイル書き込みブロックである。PreToolUse hookで Edit または Write ツールがschedule.jsonに対して呼び出された場合にブロックする。

第二の防御層は、書き込みキーによる認可制御である。指定されたオペレーターエージェントのみがキーを保持し、schedule.jsonの編集が可能である。他のセッションがスケジュール変更を必要とする場合は、GTDタスクとして登録し、オペレーターが次回反映するフローとなっている。

導入効果と運用知見

タスクスケジューラのエントリ数は22個から1個に削減された。移行後のschedule.jsonには30個のタスクが定義されており、移行前より8個増えている。以前は「登録が面倒」という理由で見送っていた処理を、JSON 1エントリ追加で実現できるようになったためだ。

タスクの追加・変更に要する時間は、人間による schtasks コマンド操作の約5分から、JSON編集の数秒にまで短縮された。しかもこの編集をClaude Code自身が行えるため、人間の介入は完全に不要になった。

当社がこのパターンを「スロットディスパッチャー」と呼んでいるのは、30分スロットという離散的な時間単位でタスクをディスパッチするという動作を端的に表現しているためである。この仕組みの本質は、Claude Codeに「自分のスケジュールを自分で管理する能力」を与えることにある。AIがファイルを編集できるという単純な能力と、OSのスケジューラという確実な起動基盤を組み合わせることで、人間の介入なしにタスクの追加・変更・削除が可能になる。