この記事はシリーズ「Claude Code で技術ブログを無人運用する — 無料章から読む仕組みの全体像」の第 1 回(全 6 回)です。
Claude Code のスケジュール実行だけで技術ブログ(Qiita 主軸・Zenn 試験投稿・X 告知・画像生成)が人手なしで回り続ける仕組みを、運用しているリポジトリの実測ログとコマンド結果で解説する連載です。Zenn 本『Claude Code で技術ブログを無人運用する』の無料章(第 1〜3 章・第 18 章・第 20 章)をもとにしており、各回は本の該当章へ戻れる導線を持ちます。掲載する数値と実行結果は各回の執筆時点で採取し直します。
仕組み全体を自分のリポジトリで組み直したい方へ: 無料章の続き(記事の型・品質ゲート・Qiita/Zenn/X の公開設計・無人実行・計測)を全 20 章の手順書として書いた Zenn Book を公開しています(有料 500 円・試し読みあり)。
シリーズ全体の目次
- 第 1 回 1 日 4 スロットのスケジュール実行で、技術ブログが人手なしで出続ける仕組みの全体像(この記事)
- 第 2 回 ガバナンスの土台は公開ベースを当てるだけ。その上にブログ層を足す境界の引き方
- 第 3 回 無人ブログの現在地を 3 か所から数える: 台帳・公開ログ・エンゲージメント実績(公開予定)
- 第 4 回 Zenn がデプロイ成功なのに公開されない・Qiita の 429: 無人運用で踏んだ公開まわりの症状(公開予定)
- 第 5 回 X の 403・承認待ちでスロットが空振り・git push が 403: 無人運用の拡散と運用の症状(公開予定)
- 第 6 回 本を Claude Code に読ませて自分のリポジトリに再現する: 読む順路・検証済み SHA・改訂の追随(公開予定)
はじめに
Claude Code のスケジュール実行に「記事を書いて公開して」と頼むところまでは、プロンプト 1 行で始められます。難しいのはその先です。1 日に何度も起動するうちの 1 回が何も残さずに消えたとき、あるいは記事を出しすぎてプラットフォームに止められたとき、無人の運用ではそれを見ている人がいません。
本リポジトリは、Qiita を主軸に、Zenn への試験投稿、X での告知、告知用の画像生成までを回している技術ブログの運用リポジトリです。起動は 1 日 4 回だけで、記事の執筆から公開・告知までが人手なしで続いています。全 6 回の連載の入口として、この回ではその 4 回の起動のあいだに何が起き、どこで止まり、止まったことにどう気づくのかを、実際のスロット表と観測コマンドの出力で追います。
立てる問いは 1 つです。人手ゼロで記事が出続けるために、何を機械の側に持たせたのか。 答えは末尾の「結論」で出します。
対象読者は、技術ブログを持っている(持ちたい)エンジニアで、Claude Code を業務で使っていて、記事の執筆・公開・告知を人手なしで回したい人です。
掲載した出力は、2026-09-25 12:09 JST に本リポジトリのコミット f7f0721 で採取したものです。どれも判定と読み取りだけのコマンドで、投稿や課金を伴う処理は実行していません。
起動するのはルーティン 1 本、時刻を判定するのはリポジトリ
起動を担うのは Claude Code のルーティン(クラウド上で定期実行される Claude Code のセッション)1 本で、名前は blog-dispatch です。cron には 0 0,3,7,11 * * * を入れています。
この式は UTC で書いています。公式ドキュメント によると、ルーティンのスケジュールは「毎日」「平日」などのプリセットならローカル時刻で入力して自動変換され、細かい指定はカスタムの cron 式で行います。本リポジトリが使っているのは後者で、2026-07-26 に設定したときはタイムゾーンを選ぶ欄が無く、入力した式は UTC として解釈されました。その実測を docs/cloud-schedule-prompt.md に残し、JST 版の 0 9,12,16,20 * * * は「入力しない」と明記しています。JST のつもりで入れると 9 時間ずれ、起動がスロット表に無い時刻に当たって 4 回とも何もせずに終わるためです。
ルーティンに貼っているプロンプトには判断ロジックがありません。書いてあるのは「.claude/skills/hourly-dispatch/SKILL.md を読み、その Step 1〜4 に最後まで従うこと。判断はそのファイルを唯一の正とすること」という内容だけで、時刻すら見ません。何時に何をするかは、この SKILL.md のスケジュール表が決めます。
| SLOT (JST) | 本処理 | 曜日などで付く処理 |
|---|---|---|
| 09:00 | collect-news(AI 関連ニュースを集めて記事の材料にする) | 月曜は週次の計測と分析、土曜は改善系のレーンなど |
| 12:00 | write-article(記事を 1 本書いて PR にする) | なし |
| 16:00 | write-article | なし |
| 20:00 | collect-news | 月曜は週次の振り返り、それ以外は改善 Issue を 1 件まで消化 |
時刻の判定は SKILL.md の Step 1〜3 が行います。TZ=Asia/Tokyo date で JST の時と分を取り、分を 30 分単位に丸めてスロットキーを作り、上の表で引きます。今回の採取時刻に当てはめると次のようになります。
$ TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M %Z'
2026-09-25 12:09 JST
12:09 を丸めると 12:00 になり、表では write-article に当たります。公式ドキュメントにあるとおりルーティンは数分遅れて起動することがありますが、丸めがあるので同じスロットに収まります。逆に、設定の誤りなどで表に無い時刻に起動しても「対象外」として何もせずに終わります。cron は UTC、判定は JST という二重構造にしているのは、時刻の判定をルーティンの設定画面ではなく、PR で変更履歴が残るリポジトリの側に置くためです。
どのスロットも同じ 15 個の先頭ステップから始まる
スロットキーで本処理を引く前に、どのスロットでも同じ 15 個の先頭ステップが番号順に走ります。役割で分けると 4 つの群になります。
| 群 | ステップ | 役割 |
|---|---|---|
| 観測 | 0 dispatch-heartbeat / 0b check-publish-stall / 0c check-book-deploy / 0d note Cookie の取り込み | 直前のスロットの完走、記事の公開、本の予約デプロイを判定し、別レーンで使う Cookie を受け取る(結果にかかわらず後続は止めない) |
| 公開 | 1 publish-qiita | 公開待ちの記事を上限の範囲で Qiita へ POST し、X へ告知する |
| 拡散 | 2 check-zenn-x-queue / 3 post-x-speed / 4 post-x-usage / 5 watch-kinako-x / 6 x-engagement-auto-follow / 8 x-article-announce / 9 collect-x-announcement-metrics / 11 x-book-promo | Zenn 記事の告知、速報、リポストとフォロー、告知の再試行と計測、本の紹介 |
| 応答 | 7 check-qiita-comments / 10 check-x-replies | Qiita のコメントと X の返信に対応する |
番号の並びは優先度で決まっています。X への書き込みは新しい記事の告知が本業なので、新規公開の告知(1)と告知の再試行(8)を先に置き、他の人からの返信への対応(10)と本の紹介(11)を最後に回しています。
起動から投稿先までを 1 枚にすると、流れは次のとおりです。
分岐は、スロットキーで本処理を引くところと、main に入ったあと投稿先へ分かれるところの 2 か所しかありません。書き上がった記事は public/ に id: null のまま在庫として置かれ、どのスロットでも先頭ステップ 1 がそれを拾って Qiita へ出します。記事を書くスロット(12:00・16:00)と記事が外へ出る瞬間が切り離されているので、在庫がある限り、執筆が 1 回飛んでも公開のペースは変わりません。
15 個もステップがあれば、どれかは落ちます。それでもスロット全体が止まらないのは、SKILL.md が次の 3 つを原則として持っているからです。
- 全ステップが冪等: 対象が無ければすぐ終了し、途中で落ちても次のスロットが同じ対象を拾い直します。そのためリトライの仕組みは組んでいません。
- 止めるのは状態ファイルの破損だけ: キューなどの JSON が壊れたときに空の値で埋めて進むと、投稿待ちが丸ごと消えます。そこだけは当該ステップを止め、他のステップは続けます。
- 上限に当たったら繰り越し: 日次の上限で止まったステップは正常終了として扱い、翌スロットや翌日に消化します。
3 つとも「失敗しても次の機会がある」ことを前提にしています。1 日 4 回同じ先頭ステップを回す構成そのものが、リトライ機構の代わりになっています。
変更はすべて PR を通って main に届く
スロットが作る変更は記事ファイルだけではありません。告知のキュー、コメント対応の台帳、クールダウンの期限など、先頭ステップが更新する状態ファイルも含めて、すべて作業ブランチから PR を作り、squash マージで main に入れます。
main への直接 push は物理的に通りません。.claude/settings.json の PreToolUse フックに登録した .claude/hooks/pre-tool-use-router.sh が、Bash の git push を .claude/hooks/pre-git-push-check.sh に回し、main と master 宛ての push を拒否します。GitHub の MCP ツールでファイルを main へ直接書き込む経路も、同じルーターが塞いでいます。
無人運用でこの制約を外せないのは、main が公開の入口を兼ねているからです。Zenn は main へのコミットを受けてデプロイするので、main に直接入った変更は、PR という区切りを経ないまま Zenn のデプロイに乗りえます。PR を 1 枚挟めば、スロットごとの変更がひとかたまりの差分として残り、push のたびに同じフックで検査を掛けられます。記事を公開する PR は、執筆パイプラインの中で品質ゲートを通してあるため、AI レビューを依頼せずにすぐマージします。squash のコミットメッセージに [ci skip] を付けるかどうかで、そのマージを Zenn のデプロイに乗せるかを切り替えています。
投稿先の上限は、定数としてコードが持っている
main に入った記事は Qiita・Zenn・X の 3 つへ出ていきます。どれにも上限があり、そのすべてをルール文書の文章ではなく、スクリプトの定数とゲートで守っています。
| 投稿先 | 上限 | 守っている場所 |
|---|---|---|
| Qiita | 新規 POST は 1 日(JST)2 本、直近 168 時間で 14 本、1 スロット 1 本 |
scripts/publish-qiita.js(QIITA_DAILY_MAX=2・QIITA_WEEKLY_MAX=14・QIITA_NEW_LIMIT=1。既定値は scripts/lib/qiita-publish-logic.js) |
| Zenn 記事 | 最後の公開から 2 日以上あける |
scripts/check-zenn-interval.js の MIN_INTERVAL_DAYS.article=2(npm run check:zenn) |
| Zenn 本 | 最後の本から 8 日以上あける(記事と本は別枠) | 同じスクリプトの MIN_INTERVAL_DAYS.book=8(npm run check:zenn:book) |
| X | 記事の告知は Qiita の公開に連動(告知の再試行は 1 日 2 件)、返信は 1 日 3 件 |
scripts/lib/x-announce-queue.js(X_ANNOUNCE_DAILY_MAX=2)・scripts/check-x-replies.js(X_REPLY_DAILY_MAX=3) |
Qiita の週 14 本は Qiita が公表している値ではなく、本リポジトリの公開ログから割り出した値です。日次 2 本はそれを 7 日に均等に配ったもので、割り出した経緯は第 4 回で扱います。X の告知は、記事の URL を本投稿ではなく最初のリプライに置く構成が基本です。ただし URL を本文とリプライのどちらに置くかは 2 パターンで比較しているので、送信前の検査(scripts/lib/x-url-reachability.js)は「どちらか一方に必ず URL がある」ことを確かめ、どちらにも無ければ投稿そのものを拒否します。
Zenn のゲートは、実際には次のように出力します。
$ npm run check:zenn
ZENN_OK type=article days=2 last=2026-09-23 src=zenn-api(実公開・トラッカーに記録漏れ) since_publish=49.5h today=2026-09-25 (>= 2日経過・デプロイ可)
$ npm run check:zenn:book
ZENN_BLOCK type=book days=7 last=2026-09-18 today=2026-09-25 next_ok=2026-09-26 (< 8日・この種別はデプロイしない) remote=skipped(fetch-failed)
記事と本は別枠なので、同じ日に記事は ZENN_OK、本は ZENN_BLOCK になります。next_ok には次にデプロイできる日が入るので、BLOCK は「捨てる」ではなく「待つ」という意味です。
記事の行にある src=zenn-api(実公開・トラッカーに記録漏れ) は、デプロイを記録するファイル(トラッカー)に最新の公開が載っておらず、Zenn から取得した実際の公開日を採用したことを示しています。Zenn は上限に当たってもエラーを返さず、デプロイ成功の表示のまま記事を公開しないことがあるため、トラッカーは「デプロイした記録」でしかありません。そこで実公開の情報が取れるときはそちらも突き合わせ、ここでは実公開から 49.5 時間たっていて、Zenn の判定窓である 24 時間を外れていることまで確かめています。本の行の remote=skipped(fetch-failed) は、この実行では実公開の取得に失敗し、トラッカーだけで判定したことを表します。突き合わせができなかった事実を黙って飲み込まず、出力に残す作りです。
この記事自体が Zenn に出ない理由
採取時点の check:zenn は ZENN_OK を返しています。それでもこの記事は Zenn には出ず、Qiita だけで公開されます。連載の定義ファイル(content/series/unattended-blog-book-guide.json)が "zennTestPost": false を持っていて、連載の回は check:zenn の結果を見ずに Qiita 専用として扱われるからです。
Zenn の 2 日に 1 本の枠は、Zenn の公開制限の閾値を測るための試験投稿枠です。連載の回がたまたまこの枠に当たると、ある回だけが Zenn にも存在し、Zenn 側の連載の導線が存在しない記事を指す状態になります。本リポジトリでは実際に、別の連載の第 1 回がこの枠に当たって Zenn にも公開されたことがありました。いまは scripts/check-series-zenn-exclusion.js(npm run check:series-zenn)が、連載の記事に Zenn 公開の指定が入っていないかを、公開準備の段階と push 時の両方で検査しています。どこへ出すかを決めるのは記事を書いたセッションではなく、連載の定義とゲートです。
止まったことに気づく観測点は、入力側と出力側の 2 つ
ここまでの仕組みが止まったとき、ルーティンの画面は当てになりません。公式ドキュメント にも、実行一覧の緑の表示は「セッションが起動し、インフラのエラーなく終了した」ことを意味するだけで、プロンプトに書いたタスクの成功を意味しない、と注記されています。そこで本リポジトリは、先頭ステップ 0 と 0b に観測点を 2 つ置いています。
入力側: スロットが完走したか
scripts/dispatch-heartbeat.js は、スロットが最後まで走り切ったときだけ完了記録(slot_end)を logs/dispatch-heartbeat.jsonl に書きます。開始の記録は取りません。承認ダイアログで待ち続けたり認証が切れたりしてセッションが消えると、コミットしていない変更はコンテナごと消えるので、開始を書いても一緒に消えてしまうからです。検知側は「完了の記録が無いこと」そのものを停止のシグナルにします。完了記録はそのスロットがどのみち作る PR に相乗りさせるので、記録のために PR が増えるのは、何も変更が無かったスロットの 1 件だけです。
$ npm run check:heartbeat
DISPATCH_HEARTBEAT KNOWN 検知済みの停止のみ: 2026-09-24 9:00
新規の停止は無い(対応済み・対応中として扱い、再通知しない)
判定の対象は、前日の 4 スロットと、当日のうち開始から 6 時間を過ぎたスロットです。12:09 の時点では当日のスロットはまだ対象に入らないので、前日の 4 つを見て、完了記録が欠けていたのは 9:00 の 1 つだけでした。この停止は既に検知済みのため、KNOWN として再通知はしません。
6 時間という猶予にも、止まり方の実績が反映されています。スクリプトの冒頭には、2026-08-31〜09-24 の 70 スロットで所要時間の p99 が 58 分、最長が 345 分だったという実測が残っています。猶予が 90 分だった頃は、5 時間半走っていた 12:00 のスロットを 16:00 の判定が停止と誤検知し、生きているセッションと記事番号を取り合いました。本物の停止の発見が 1 スロット遅れても、誤検知で復旧作業を走らせるよりは害が小さい、という判断です。
出力側: 記事が実際に出ているか
入力側だけでは足りなかった実例があります。2026-08-29 12:07 JST を最後に Qiita の公開が 2 日続けてゼロになったとき、スロットは毎回完走していて、入力側の判定は正常でした。パイプラインは回っているのに成果物が出ていない状態を、機械の側で検知する手段が無かったのです。そのあとに足したのが scripts/check-publish-stall.js です。
$ npm run check:publish-stall
PUBLISH_STALL OK last="2026-09-24 13:17:39 JST" ago=22.9h consecutiveZeroDays=0 — 直近 22.9h 以内に公開実績あり
見ているのは logs/qiita-post-log.jsonl にある最後の新規 POST の成功で、30 時間を超えると WARN、48 時間を超えると STALL になります。1 日 2 本・最終スロットが 20:00 という運用では、正常なら公開の間隔は前日 20:00 から翌 09:00 までの約 13 時間に収まるので、30 時間を超えた時点で「丸 1 日ゼロ」が確定します。週次の上限が満杯で出ていないだけなら、停止ではなく CAPPED として区別します。
2 つの出力を並べると、前日の 9:00 のスロットは完了記録を残さなかった一方で、同じ日の 13:17 には記事が公開されています。止まったのはスロット 1 つで、公開そのものは次のスロットで続いていた、と層を分けて読めます。観測点を 2 つに分けているのは、片方だけが崩れたときにどの層で止まったのかが分かるからです。どちらの判定も結果にかかわらずスロットを止めず、停止を検知しても公開はいつもどおり試行します。
結論: 機械に持たせたのは「書くこと」より「止まる場所」と「気づく場所」だった
冒頭の問いに戻ります。人手ゼロで記事が出続けるために、本リポジトリが機械の側に持たせたものは 4 つあります。
1 つ目は、判断をプロンプトではなくリポジトリのファイルに置いたことです。ルーティンのプロンプトは「SKILL.md を読め」だけで、スロット表の変更もステップの追加も PR として履歴に残ります。cron を UTC で書いて判定を JST で行う二重構造は手間に見えますが、時刻の判定まで設定画面から引き上げたことで、起動時刻がずれても誤った処理は走らず、「何もしない」側に倒れるようになりました。
2 つ目は、書くことと出すことを分けたことです。記事は執筆スロットで在庫として main に入り、公開はどのスロットでも先頭ステップが上限の範囲で行います。前日の 9:00 のように 1 スロットが消えても、公開待ちの記事は在庫として残っていて、次のスロットの先頭ステップがそれを拾います。実際、同じ日の 13:17 には公開が出ていました。
3 つ目は、上限を文章ではなく定数とゲートにしたことです。上限に当たることは失敗ではなく繰り越しとして扱われ、どこへ出すかも記事を書いたセッションではなく定義が決めます。この記事が ZENN_OK の日に書かれても Zenn に出ないのは、その結果です。
4 つ目は、止まったことに気づく場所を入力側と出力側の 2 つに分けたことです。どちらも最初から設計していたわけではありません。承認ダイアログでスロットが丸ごと空振りし、ルーティンの画面が緑のまま気づけなかったことと、スロットは正常なのに公開が 2 日ゼロだったことのあとに、1 つずつ足したものです。
振り返ると、4 つとも記事を「書かせる」ための工夫ではなく、「止まる場所を決め、止まったら気づく」ための工夫でした。記事を書く部分は Claude Code に任せられても、どこで止まるべきかと、止まったことを誰が知るかは、機械の側に明示的に持たせない限り、どこにも存在しません。
裏を返すと、観測点はどれも一度止まったあとに足したもので、まだ起きていない止まり方には何も鳴りません。次にどこで止まりうるかを考える手がかりは、第 4 回と第 5 回で並べる、実際に踏んだ症状の一覧になります。
連載の地図
この連載は全 6 回で、3 部に分かれています。第 1 部で仕組みと現在地を、第 2 部で実際に踏んだ症状を逆引きで、第 3 部で自分のリポジトリへの再現を扱います。
| 回 | 部 | タイトル |
|---|---|---|
| 1 | 1 | 1 日 4 スロットのスケジュール実行で、技術ブログが人手なしで出続ける仕組みの全体像(この回) |
| 2 | 1 | ガバナンスの土台は公開ベースを当てるだけ。その上にブログ層を足す境界の引き方 |
| 3 | 1 | 無人ブログの現在地を 3 か所から数える: 台帳・公開ログ・エンゲージメント実績 |
| 4 | 2 | Zenn がデプロイ成功なのに公開されない・Qiita の 429: 無人運用で踏んだ公開まわりの症状 |
| 5 | 2 | X の 403・承認待ちでスロットが空振り・git push が 403: 無人運用の拡散と運用の症状 |
| 6 | 3 | 本を Claude Code に読ませて自分のリポジトリに再現する: 読む順路・検証済み SHA・改訂の追随 |
この記事と本の関係
この回は、Zenn 本『Claude Code で技術ブログを無人運用する』の第 2 章「全体像」をもとに、出力を 2026-09-25 に採り直して書き直したものです。第 2 章は本の無料章です(Zenn の本のページ)。続きにあたるのは有料章で、第 5 章が記事を 1 本書く執筆パイプライン、第 15 章がルーティンを無人で動かすための設定を扱います。本には、Claude Code に読ませると自分のリポジトリで同じ仕組みを組めるように書いた仕様パック(第 19 章)も入っています。
参照
本リポジトリ内のファイル(リポジトリは非公開のため、パスだけを示します):
-
.claude/skills/hourly-dispatch/SKILL.md: スケジュール表と先頭 15 ステップ(スケジュールの単一ソース) -
docs/cloud-schedule-prompt.md: ルーティンに貼る最小プロンプトと、cron を UTC で書く実測の注記 -
scripts/dispatch-heartbeat.js: 完了記録だけを残す入力側の観測点 -
scripts/check-publish-stall.js: 記事の公開を見る出力側の観測点 -
scripts/publish-qiita.js/scripts/lib/qiita-publish-logic.js: Qiita の上限 -
scripts/check-zenn-interval.js: Zenn の記事と本の間隔 -
scripts/check-series-zenn-exclusion.js: 連載を Zenn の試験投稿枠に乗せないゲート -
.claude/hooks/pre-git-push-check.sh: main への直接 push の拒否
外部ドキュメント:
- Automate work with routines(Claude Code Docs。スケジュールの指定方法と、実行一覧の緑の表示が意味する範囲)
シリーズの前後の記事
- ⬅️ 前の記事: なし(この記事がシリーズの最初です)
- ➡️ 次の記事: 第 2 回 ガバナンスの土台は公開ベースを当てるだけ。その上にブログ層を足す境界の引き方
仕組み全体を自分のリポジトリで組み直したい方は、無料章の続き(記事の型・品質ゲート・Qiita/Zenn/X の公開設計・無人実行・計測)を全 20 章の手順書として書いた Zenn Book(有料 500 円・試し読みあり)へどうぞ。