ラッコとまなぶです。
本業のかたわら、Webサイトやアプリを10個くらい個人で作って運用しています。
最近、claude.ai で設計したものを Claude Code に渡して、実装からレビュー、push まで勝手にやってもらう仕組みを作りました。自分用の mod で、名前はそのまま「handoff」です。1か月弱で360件ほどの設計メモがこれで流れていったので、どう作ったかを書いておきます。
今の使い方はこうです。スマホの claude.ai で「ここ直したいんやけど」と相談して、仕様が固まったら Claude が設計メモを PC のフォルダに書き出します。あとは PC 側で拾って実装して、別の Claude がレビューして、OK なら main に入る。こっちは次の日にでも「終わった?」と聞くだけです。
なんで作ったか
もともと、設計は claude.ai、実装は Claude Code と分けて使っていました。分け方自体は気に入っていたんですが、間をつなぐのが全部自分の手作業でした。
claude.ai で決めた仕様を Claude Code にコピペする。Claude Code が「AとBどっちにしますか?」で止まったら、画面をスクショして claude.ai に貼って相談する。返ってきた答えをまた Claude Code に貼る。リポジトリが10個近くあるので、そのうちどれがどこまで進んだのか自分でも分からなくなってきました。
やってることは、ほぼ右から左に流すだけです。家に小さい子どもがいて PC の前に座れる時間も限られているので、この「流すだけの時間」をなんとかしたかった、というのが動機です。
全体の流れ
[スマホ / claude.ai]
│ 設計が固まったら設計メモを書き出す
▼
D:\13_Claudehandoff\<プロジェクト名>\20261025-xxxx.md
│ 15秒以内に検知
▼
[Claude Code + handoff mod]
├─ 実装役 : claude -p(ブランチ handoff/<メモ名> で実装)
├─ レビュー役: claude -p(別プロセス)
│ ├─ OK → mod が main にマージして push
│ ├─ 要修正 → 修正メモを自動で出して再実装(最大3回)
│ └─ 止まれ → 人間に聞く
▼
STATE.md / 案件ごとの状態ファイル
▲
└─ claude.ai から「終わった?」と聞くときに読む
チャットと Claude Code を直接つなぐ方法もいろいろ考えたんですが、結局「決まったフォルダに Markdown を置く」だけにしました。これならどっちの側も相手のことを知らなくていいし、手で置いても同じように動きます。
フォルダの中身
D:\13_Claudehandoff\
├─ projects.json … プロジェクト名とリポジトリの対応表
├─ decision-rules.md … 止まる・進むの判断ルール(全プロジェクト共通)
├─ STATE.md … 全案件の一覧(mod が書く)
├─ status.json … 全案件の状態(mod が書く)
├─ mod\ … mod 本体
└─ <プロジェクト名>\
├─ 20261025-xxxx.md … 未着手の設計メモ
├─ done\ … 終わったメモ
├─ reports\ … 結果レポート、レビュー、自己判断の記録
├─ state\ … 案件ごとの状態
└─ _hold\ … 時間切れで保留にしたメモ
projects.json はこれだけです。
{
"root": "D:\\13_Claudehandoff",
"projects": {
"sansu-mikke": { "repo": "D:\\12_Claude\\07_sansu-mikke", "push": "main", "watch": true },
"shikakurabe": { "repo": "D:\\12_Claude\\02_Application\\shikakurabe", "push": "main", "watch": true },
"kengaku-gas": { "repo": "D:\\12_Claude\\06_Kengaku_GAS", "push": "main" }
}
}
push には main(マージして push)、branch(ブランチだけ push)、none(push しない)を書けます。今は全部 main です。
設計メモの書き方
claude.ai のプロジェクト指示に、次のテンプレートで書き出すよう入れています。
# <タイトル>
## 目的
(何のためにやるか、1〜3行)
## 決定事項
- (チャットで決めた仕様を箇条書き)
## 対象範囲
- (触りそうな画面・ファイル・機能)
## やらないこと
- (今回スコープ外にするもの)
## 完了条件
- (これができたら完了、と判定できる条件)
## 事前許可
allow: (チャットで導入OKと決めたパッケージ。なければこの欄ごと省略)
## 補足
- (参考情報、既存コードの注意点など。なければ「なし」)
あわせて、書き出しのルールも指示に書いてあります(抜粋)。
## 設計メモの書き出し
個人開発の設計がまとまったら、確認を挟まずに設計メモを書き出す。
- 書き出し先:D:\13_Claudehandoff\<プロジェクト名>\
- どのプロジェクトか会話から明らかでなければ、書き出す前に聞く
- ファイル名は「YYYYMMDD-英小文字とハイフンの短い名前.md」
(ブランチ名にも使われるため日本語不可)
- 新しいライブラリの導入を会話で決めた場合だけ、「事前許可」欄に
行頭から「allow: パッケージ名」と書く。決めていないものは書かない
- 1つのメモ=1つの実装単位。大きい場合は分割して複数出す
「確認を挟まずに」は最初入れていなくて、毎回「書き出してよろしいですか?」と聞かれていました。ここで1往復増えるのが地味にだるかったので足しています。
ファイル名がそのままブランチ名になるので、日本語は禁止です。最初はタイトルをそのままファイル名にしていて、ブランチ名が文字化けしかけたのでやめました。
テンプレートの中では「やらないこと」と「完了条件」が一番効いています。これがないと実装役が気を利かせて周りも直し始めるし、レビュー役も何を基準に OK を出せばいいのか分からなくなります。
mod の中身
mod は Claude Code のデスクトップアプリで動く拡張で、~/.claude/settings.json の env から全セッションに読み込ませています。TypeScript と PowerShell で3,500行くらいになりました。そのうち半分は register.ts です。
mod\
├─ .claude-plugin\plugin.json
├─ hooks\
│ ├─ register.ts … 本体(検知・実装・レビュー・push)
│ ├─ panel.tsx … パネル表示
│ ├─ robust.ts … タイムアウトとハートビート
│ ├─ state.ts … STATE.md と状態ファイルの書き出し
│ └─ push.ts
├─ scripts\ … watcher の登録・起動
└─ tests\
実装は claude -p に任せる
セッションが始まると定期的にフォルダを見に行って、古いメモから1件ずつ拾います。実装そのものは、ヘッドレスの claude -p を子プロセスで起動してやらせています。
const a = [
'claude', '-p', '--output-format', 'stream-json', '--verbose',
'--permission-prompts', 'none',
'--strict-mcp-config', '--disable-slash-commands',
'--max-budget-usd', String(budget), // 実装 5ドル / レビュー 2ドルまで
'--add-dir', `${ROOT}/${name}`, // メモとレポートを読めるように
'--add-dir', ROOT, // decision-rules.md も読めるように
]
MCP もスキルも読み込ませず、なるべく軽く起動しています。1回の上限額も決めておかないと、ループしたときが怖いので。
子にはできないことを決めておく
実装役に許すコマンドと禁止するコマンドはこうしています。
const IMPL_ALLOW = [
'Bash(git *)', 'Bash(npm run *)', 'Bash(npm test*)', 'Bash(npx --no-install *)',
'Bash(node *)', 'Bash(python *)', 'Bash(ls *)', 'Bash(grep *)', /* … */
]
// 戻せない操作は丸ごと拒否。子は「止まれ」で報告する
const IMPL_DENY = [
'Bash(git push*)', 'Bash(git reset --hard*)', 'Bash(git clean*)', 'Bash(git branch -D*)',
'Bash(git * --force*)', 'Bash(git restore*)', 'Bash(git stash drop*)',
'Bash(rm *)', 'Bash(del *)', 'Bash(npm uninstall*)',
// 受け渡しフォルダは読むだけ。レポートと status.json は mod だけが書く
'Edit(D:/13_Claudehandoff/**)', 'Write(D:/13_Claudehandoff/**)',
].join(',')
push は子には一切させません。レビューが OK になったあと、mod 自身が projects.json の設定どおりに push します。--force はどこでも使っていません。
インストール系のコマンドも最初は全部禁止で、設計メモの allow: に書いたパッケージだけ、その案件の間だけ許可します。
for (const p of pkgs ?? []) {
for (const head of ['npm install', 'npm i', 'pip install', 'yarn add', 'pnpm add', /* … */]) {
out.push(`Bash(${head} ${p})`, `Bash(${head} ${p} *)`)
}
}
プロンプトに「push しないでね」と書くだけだと、どこかで破られそうで寝る前に不安になります。ツールの許可設定で、そもそもできないようにしておくと安心して放っておけます。
レビューは別プロセスで
実装が終わったら結果レポートを書かせて、別の claude -p にレビューさせます。自分で書いたコードを自分でレビューさせても、だいたい「問題ありません」で終わるので分けました。レビュー役は読むことしかできません。
const REVIEW_ALLOW = 'Read,Glob,Grep,Bash(git diff*),Bash(git log*),Bash(git show*),Bash(git status*)'
結果は「OK」「要修正」「要判断」のどれかで返してもらいます。OK なら mod がマージして push し、メモを done\ に移します。要修正なら <メモ名>-fix1.md を自動で作って、同じブランチでもう一度実装させます。これを3回やってもダメなら、止めずに「残課題ありで完了(未push)」にして、ブランチだけローカルに残します。
レビュー結果は Markdown で残ります。たとえばこんな感じです(抜粋)。
# レビュー結果: 20261025-countdown-dots-collapse.md
- 判定: OK
- 修正ループ: 0 回目 / 上限 3 回
## 消費量(total_cost_usd 相当)
- implement: $1.1854
- review: $0.1225
## push
- 設定: main
- 結果: main にマージして origin/main に push、専用ブランチを削除しました
1件いくらかかったかが毎回出るので、だいたいの相場も分かってきました。
判断ルールを1ファイルにまとめた
作ってみて一番変わったのはここです。
最初のころは、Claude Code が判断待ちで止まるたびに、パネルをスクショして claude.ai に聞いていました。自動化したはずなのに、結局そこで人間が挟まっている。止まる理由を眺めていると、ほとんどが「ボタンの文言どうしますか」「テスト落ちたけどどう直しますか」くらいのもので、正直どっちでもいいものばかりでした。
それで考え方を逆にして、「止まっていい条件」だけを書き出して、それ以外は全部自分で決めて進めてもらうことにしました。ルールは全プロジェクト共通の decision-rules.md に置いて、実装役とレビュー役のプロンプトから読ませています。
止まっていいのは次の5種類だけです。
| 種別 | 内容 |
|---|---|
irreversible |
戻せない操作(本番データ削除、履歴の書き換え、未コミット変更の破棄) |
cost |
お金がかかるもの(有料 API、課金プラン、上限額に達した) |
release |
公開・リリース(ストア申請、本番デプロイ) |
secret |
秘密情報(API キー、認証情報) |
library |
未許可の新規ライブラリ |
逆に、設計メモに書いていない細かい仕様、DDL の変更(既存データを消さないもの)、テストが落ちたときの直し方なんかは自分で決めてもらいます。迷ったら設計メモの「目的」に一番合う方、自信がなければ後から戻しやすい方を選ぶ、とも書いてあります。これ以外の理由で止まろうとすると、mod が1回だけ「推奨案で進めて」と差し戻します。
その代わり、決めたことは理由と一緒に全部書き残してもらいます。実際の記録から2件だけ載せます。
- [実装] 折りたたんだ月は DOM から消さず、hidden 属性で隠すことにした
(理由: 開け閉めのたびに描き直さずに済み、アニメーションも付かないため)
- [実装] ボタンを週末ドットの帯の中に置き、帯の aria-hidden は月の並びと凡例に付け直した
(理由: aria-hidden の中にボタンを置くと読み上げで操作できなくなるため)
この aria-hidden の件は、自分で書いていたら見落としていたと思います。こういう判断を勝手にしてくれるなら、途中で聞かれなくていいや、と思えるようになりました。
同じ案件では、レビュー役が「npm test が時間切れで落ちているが、今回触っていないテストで、単独なら通る。別の案件として扱うのが妥当」と判断して OK を出していました。実際その後、時間切れを直すメモを別に流して片付いています。こういうのを見ていると、止めずに進めてもらう方針でよかったなと思います。
どうしても止まるときは、スクショなしで判断できるように書き方を決めてあります。
【論点】何を決めたいか(1〜2行)
【選択肢】
A: 〜(影響:〜)
B: 〜(影響:〜)
【推奨】A。理由:〜
【戻せるか】A/B それぞれ、後から変えられるか
【人間にやってほしいこと】(手作業が必要な場合のみ)
【返答の例】/handoff reply A
返事は /handoff reply A や /handoff reply OK allow:dayjs と打つだけです。
状態はファイルに書いて、チャットから読む
mod は状態が変わるたびに STATE.md を書き直します。
# handoff の状態
更新: 2026-10-25 22:35(JST)
- 止まれ案件で停止: 0 / 停止(要対応): 0
- 実装中: 0 / 未着手: 0 / 保留: 0
- 残課題ありで完了(未push): 0 / 完了(直近3日): 31
claude.ai に「終わった?」と聞くと、これを読んで教えてくれます。スマホへのプッシュ通知は作っていません。通知が来ると気になって見てしまうので、自分が聞きたいときに聞く方が合っていました。
止まらないようにする
watcher
よく触るプロジェクトだけ、画面を出さない Claude Code のセッションを常駐させています(watcher と呼んでいます)。Windows のタスクスケジューラでログオン時に起動して、落ちたら10秒後に立ち上げ直します。
60秒ごとにハートビートを書いていて、45分なにも進まなければ子プロセスを止めて1回だけやり直します。それでもダメなら _hold\ に移して次のメモに進みます。1本詰まったせいで全部止まる、というのを避けたかったので。
「進んでいる」の判定でハマった
最初は、子プロセスが何か出力したら進んでいるとみなしていました。ところが、ツールの実行中は30秒ごとに tool_progress という行が出るので、コマンドが固まっていても「進んでいる」ことになり、いつまでも時間切れになりませんでした。
今は、作業ツリーのファイルが変わった時刻と、tool_progress 以外の行が出た時刻の、新しい方を見ています。
そのほかハマったところ
GitHub でメールアドレスを非公開にしていると、作者メールが noreply でないコミットは push で弾かれます。これは push 前に未 push のコミットの作者メールを調べて、引っかかったらマージも push もせずに止めるようにしました。作者を自動で書き換えることもできますが、履歴の書き換えはやりたくないので人間がやります。
handoff 自体が途中で落ちると、作業ブランチに未コミットの変更が残ります。次に拾ったとき、それが自分の途中の作業なのか、人間がいじっている途中なのかを見分けないといけません。今は「今のブランチが handoff/<メモ名> で、変わっているファイルが全部そのメモの『対象範囲』に入っていたら自分の途中の作業」とみなして、コミットして続けます。それ以外は止まります。設計メモの「対象範囲」をこんなところで使うとは思っていませんでした。
mod 本体を編集すると読み込み直しが走って、実行中の子プロセスまで止まります。何度かやらかしたので、今は watcher を止めてから直すようにしています。
あと、同じアプリでもアプリ本体とデータでリポジトリが分かれているものは、別のプロジェクトとして登録しています。
おわりに
今は、寝る前にスマホで相談しておくと、朝には main に入っている、というくらいまで来ました。自分がやるのは「何を作るか決める」ところだけです。
まだやりたいことはあって、進行中の案件をもう少し見やすくしたいのと、実装役とレビュー役でモデルや effort をどう使い分けるかは詰めきれていません。そのあたりはまた書きます。
