Claude Code は呼べば動く。しかしビジネスの現場では、呼ばれなくても動くことが必要になる。
スクレイピング、週次レポート、メール下書き生成。これらはいずれも「誰かが手動でトリガーを引く」設計のままでは、日々の運用負荷が積み重なって自動化の恩恵が薄れる。八雲の内部システム Synapse は現在、5つの launchd ジョブが macOS 上で常時稼働しており、Notion の承認フローと組み合わせて「人間が承認した作業だけを AI が自動実行する」仕組みを回している。
本稿では、その設計と実装で判明した落とし穴を共有する。
課題: cron では足りないもの
定期実行といえば cron が真っ先に挙がる。しかし Claude Code を使った自動化では cron が通用しない場面が出てくる。
まず環境変数の問題がある。gws CLI(Google Workspace 操作用の自社 CLI)は GOOGLE_WORKSPACE_CLI_CONFIG_DIR という環境変数で認証プロファイルを切り替える設計になっており、ターミナルから source scripts/shared/env.sh を実行して初めてこの変数がセットされる。cron で直接スクリプトを呼んでも、この変数は存在しない。
次にGUI 認証の問題がある。自動操作に agent-browser を使う場合(たとえばクラウドソーシングのスクレイピング)、Chrome プロファイルが適切なキーチェーン設定で起動していないと認証が通らない。--password-store=basic --use-mock-keychain フラグが必要で、cron の実行コンテキストでは挙動が変わることがある。
さらにPATH の継承がない。Claude Code 自体(claude バイナリ)は ~/.claude/local/ に置かれており、標準 PATH に含まれない。ターミナルの ~/.zshrc でセットしている PATH をそのまま使えると思ったら使えない、というのが cron の典型的な落とし穴だ。
アプローチ: macOS の launchd で定期実行
macOS の launchd(LaunchAgent)は cron の上位互換に近い。plist ファイルで各ジョブを宣言し、~/Library/LaunchAgents/ に配置することで、ログイン後の常駐プロセスとして登録できる。
重要な利点は EnvironmentVariables キーで環境変数を明示的に注入できることだ。たとえばスクレイピングジョブ(com.yakumo.synapse.scrape.plist)では以下のように設定している。
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/t.morimoto/.claude/local:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>/Users/t.morimoto</string>
</dict>
~/.claude/local を PATH の先頭に置くことで、launchd 起動のプロセスからも claude コマンドが呼べるようになる。
スケジューリング方式は2種類使い分けている。StartInterval(秒数指定のポーリング)と StartCalendarInterval(時刻指定の cron 相当)だ。Notion の承認を5分以内に検知したいタスク同期ジョブには StartInterval=300 を採用し、スクレイピング(毎日3回)や AI レビュー(毎日 4:00)には StartCalendarInterval を使っている。
plist は config/launchd/ をリポジトリ SSOT として管理し、~/Library/LaunchAgents/ へのコピーと launchctl load はインストール手順書に従って手動で行う設計にしている。設定変更時は launchctl unload → コピー → launchctl load の順で再ロードが必要だ。
Notion Tasks DB を polling して「承認済」タスクを実行
定期実行だけでは「何を実行するか」の判断が人間から切り離せない。Synapse では Notion の Tasks DB を承認フローのハブとして使っている。
ステータス遷移は単純だ。
レビュー待ち → 承認済 → 完了
│
└──差し戻し → レビュー待ち
task_sync.py が5分ごとに Notion をポーリングし、ステータス=承認済 のタスクを取得して対応するスキルをディスパッチする。実行完了後はステータスを 完了 に更新する。
人間が Notion 上で「承認済」にするだけで AI が動く設計だ。「◯◯を承認したよ、出して」とチャットで言ってもよいし(Claude 起点)、Notion のボタンをポチッと押すだけでもよい(Notion 起点のポーリング検知)。
ただし全てのタスクを自動実行するわけではない。AnotherWorks への提案は「気になる」ボタンを人間が手動クリックしなければならない仕様のため、task_sync.py は AnotherWorks タスクを skipped 扱いにしてステータスを維持する。自動化の対象は「最後まで自動で完結できるもの」に絞り、人間操作が必要なものはステータスを保持することで Notion の一覧から見落とさない設計にしている。
実例: 稼働中の5ジョブ
現在 Synapse で稼働している launchd ジョブは次の5つだ。
| ジョブ | スケジュール | 実行内容 |
|---|---|---|
task-sync | 5分ごと | Notion 承認済 タスクを polling して自動実行 |
scrape | 毎日 0:00 / 6:00 / 12:00 | Lancers / CrowdWorks のスクレイピング |
review | 毎日 4:00 | S/A ランク案件の AI レビュー |
drive-audit | 毎週月曜 3:00 | Drive 構造の整合チェック |
contact-form | 5分ごと | お問い合わせフォームの受信・返信メール送信 |
review ジョブは GAS のスコアリング処理(毎日 3:00 JST に実行)の後に走らせる必要がある。スコアが付く前に AI レビューが走っても対象案件が空になるだけで無駄だ。過去にこのタイミング依存を見落としてスクレイピング直後に review を呼んでいた構成にしたことがあり、CrowdWorks の AI 評価が全件欠損するトラブルが発生した。現在は review を scrape から切り離して独立ジョブ化し、4:00 固定スケジュールにしている。
週次レポート生成やメール下書き生成も同様に Notion 経由でトリガーされる。たとえば週報タスクを 承認済 にすれば、Calendar / Gmail / GitHub の活動ログを集計してスプレッドシートに追記するスクリプトが自動実行される。
運用してわかった効果と落とし穴
効果として実感しているのは判断と実行の分離だ。人間は「何を承認するか」だけに集中できる。実行タイミング・手順・後処理(ステータス更新)はすべて自動化されており、夜間に回しておけば翌朝には結果が出ている。
落とし穴は3点あった。
1つ目は TCC(フルディスクアクセス)の問題だ。launchd 経由で起動したプロセスは Terminal の TCC 権限を継承しない。scripts/ 配下のファイルを読み書きするには、System Settings の Privacy & Security でバイナリ(/bin/bash / /usr/bin/env など)を明示的に許可する必要がある。最初のインストール時にこれを見落とすと Operation not permitted がログに出続ける。
2つ目は plist の SSOT 管理だ。~/Library/LaunchAgents/ の plist を直接編集すると、リポジトリ側と乖離が生まれる。変更は必ず config/launchd/ 側で行い、launchctl unload → コピー → load のワークフローを守ることにした。
3つ目はタイミング依存の設計だ。前述の review ジョブの例のように、ジョブ間に依存関係がある場合はスケジュールを明示的に分離する必要がある。暗黙のタイミング依存は障害を引き起こす。
まとめ
Claude Code を「呼んだときだけ動く」ツールから「定期的に動くシステム」に昇格させるには、launchd によるスケジューリングと、Notion を介した人間の承認フローの組み合わせが実用的だとわかった。
核心は「人間が判断し、AI が実行する」という責務の明確な分担にある。承認するかどうかは人間が決め、実行の手順と後処理はシステムが引き受ける。この分担があることで、操作ミスや実行忘れを減らしながら、判断の自律性は人間が保持できる。
関連記事: Yakumo の AI 駆動開発のリアル / スキル vs エージェント: Claude Code の責務分離