作成日: 2026-03-26 ステータス: Phase 2 実装済み・運用中 Codexレビュー: 4回実施、LGTM取得。v4はnuchi新情報反映+実地検証結果
スマホ(Telegram)からざっくりした指示を飛ばすだけで、PCで動いてるClaude Codeの各セッションに適切にタスクを振ってくれる「秘書」を作りたい。個人利用のツール。
| 選択肢 | 却下理由 |
|---|---|
| OpenClaw + LINE | OpenClawは別ツール。今のClaude Code環境(MCP・memory・スキル)が活かせない |
| LINE連携(osisdie/claude-code-channels) | ⚠️ スター3。MCP pluginではなく毎回 claude -p を起動する設計。会話コンテキストが毎回リセットされる |
| Claude Code Remote Control | 「リモコン」であって「秘書」ではない。文脈補完やオーケストレーション機能がない |
claude -p で毎回新規起動 |
5時間レートリミットを大量消費。Claude Codeは全文を読まない癖があり文脈注入が不確実 |
前提条件:
[スマホ / Telegram]
↓ ざっくり指示(例:「コールリマインダーの課金変えたい」)
[秘書セッション] ← Claude Code + Channels(Telegram)
│
│ 1. セッション特定(レジストリ参照)
│ └── ~/.claude/secretary/registry.json でセッション名を解決
│
│ 2. 文脈補完(必要に応じて)
│ ├── Second-Brain/ → 最近のセッション記録(常時自動記録)
│ ├── memory/ → 永続知識
│ └── tmux capture-pane → 該当セッションの今の画面
│
│ 3. 曖昧指示の処理(clarify → propose → dispatch)
│ ├── clarify: セッション特定不能なら候補を提示
│ ├── propose: 「こう進めるけどOK?」と提案
│ └── dispatch: 承認後に tmux.py 経由で実行
│
│ 4. 進捗報告(edit_message で随時更新)
│ └── ステータスメッセージを1つ作成し、edit_message で更新し続ける
│
│ 5. 結果の取得と報告
│ ├── tmux.py wait → 完了/質問/permission を検知
│ ├── tmux.py read → 応答テキスト取得
│ ├── edit_message でステータス更新(通知なし)
│ └── 完了/ブロック時は新規 reply で通知(プッシュ通知あり)
│
[作業セッション群 on tmux]
├── line-call-reminder: コールリマインダー開発
├── caprate-map: 不動産分析アプリ
└── ...
tmux session名やSecond-Brainファイル名からの推測だけでは、誤ったセッションに指示を送るリスクがある。
~/.claude/secretary/registry.json{
"sessions": [
{
"session_name": "line-call-reminder",
"repo_path": "/Users/aiharataketo/projects/line-call-reminder",
"aliases": ["コールリマインダー", "LINE Bot", "電話リマインド"]
}
]
}
aliases でざっくり指示からの名前解決を補助秘書の会話文脈はセッション再起動で失われてもいい(外部に全知識がある)。 ただし実行中のジョブは外部ファイルに記録する(何をどこに投げたか忘れると復旧できない)。
| 情報 | 保存場所 |
|---|---|
| プロジェクトの背景・教訓 | memory/ |
| 最近のセッション内容 | Second-Brain/(常時自動記録) |
| 各セッションの今の状態 | tmux capture-pane |
| ユーザーの好み・ルール | CLAUDE.md(起動時に自動読み込み) |
~/.claude/secretary/jobs.json{
"jobs": [
{
"job_id": "j-20260326-001",
"message": "課金モデルをサブスク制に変更して",
"target_session": "line-call-reminder",
"status": "SENT",
"chat_id": "7789180125",
"status_message_id": "100",
"awaiting_reply_kind": null,
"blocked_reason": null,
"created_at": "2026-03-26T22:35:00Z"
}
]
}
status: PENDING → SENT → DONE / FAILED / BLOCKED
status_message_id: 進捗報告用のTelegramメッセージID(edit_message の対象。詳細はv4「進捗報告プロトコル」参照)
速い・軽い 遅い・重い
────────────────────────────────────────────────→
レジストリ → tmux → Second-Brain → memory → recall
台帳検索 今の画面 最近の全記録 永続知識 最終手段
(常時自動記録)
Second-Brainは常時自動記録される(セッション終了時ではない)。
制約:
tmux capture-pane で取得するaliases で補完ユーザーの入力は雑である。これは前提であり、システムが対応する。
| モード | 発動条件 | 動作 |
|---|---|---|
| clarify | セッション特定不能、指示が曖昧 | Telegramで候補を提示して確認 |
| propose | セッション特定OK | 「こう進めるけどOK?」とTelegramに返す |
| dispatch | ユーザーが承認 | tmux.py 経由で実行 |
秘書は自動でdispatchしない。必ずproposeを挟む。
2通目が来た時、1通目のジョブとの関係を推測して:
tmux.py(~/.claude/skills/tmux/scripts/tmux.py)の責務はTUI入出力の安定化のみ。セッション管理やジョブ管理は秘書セッション側の責務。
| 責務 | 担当 |
|---|---|
| TUI への確実な入力 | tmux.py send |
| 処理完了/ブロック検知 | tmux.py wait |
| 応答テキスト取得 | tmux.py read(末尾50行。長い応答は途中欠落の可能性あり) |
| セッション管理 | 秘書セッション(レジストリ) |
| ジョブ管理 | 秘書セッション(ジョブ台帳) |
| wait の結果 | ジョブ状態 | ステータス更新 | Telegram通知 |
|---|---|---|---|
ready |
→ DONE | edit_message: ✅完了 |
新規reply: 結果要約(プッシュ通知) |
ask_user_question |
→ BLOCKED | edit_message: ⚠️質問待ち |
新規reply: 質問内容を転送(プッシュ通知) |
permission_modal |
→ BLOCKED | edit_message: ⚠️権限待ち |
新規reply: 権限内容を転送(プッシュ通知) |
timeout (stderr) |
→ FAILED | edit_message: ❌タイムアウト |
新規reply: タイムアウト通知(プッシュ通知) |
session_gone (stderr) |
→ FAILED | edit_message: ❌セッション消失 |
新規reply: セッション消失通知(プッシュ通知) |
[作業セッション] permission_modal 発生
↓ tmux.py wait が "permission_modal" を返す
[秘書セッション] read で内容確認 → Telegram に転送
↓
[スマホ] 「OK」と返信
↓
[秘書セッション] tmux.py send SESSION "1" → wait で再待機
ask_user_question も同様(read で質問内容を見て、ユーザーの回答を send で転送)。
allowedTools を広めに設定(permission prompt自体を減らす)allowlist policy に切り替える1. Telegram で @BotFather → /newbot → トークン取得
2. Claude Code で:
/plugin marketplace add anthropics/claude-plugins-official
/plugin install telegram@claude-plugins-official
/telegram:configure <トークン>
3. セッションを再起動(プラグインはMCPサーバーとして自動起動する。--channels フラグは不要)
4. Telegram から Bot にDM → ペアリングコード取得
5. /telegram:access pair <コード>
6. /telegram:access policy allowlist ← 必須!
注意: Telegramプラグインはグローバルインストールされたプラグインとして全セッションで有効になる。特定のセッションだけに限定する仕組みは現時点では無い。秘書セッションだけがTelegramに応答するよう、秘書CLAUDE.mdで制御する。
LaunchAgent(macOSのログイン時自動起動の仕組み)は「tmuxセッションを作るだけで即終了」し、tmuxの中で while true ループがClaudeを監視する二層構造。
PC再起動 → ログイン → LaunchAgent起動
↓
start-secretary.sh
├── tmux has-session -t secretary → 既存なら何もしない(多重起動防止)
└── 新規なら: tmux new-session + run-claude.sh
↓
while true ループ
├── claude 起動
├── クラッシュ検知 → 再起動
└── 連続失敗5回 → 30分待機
| ファイル | 配置先 | 役割 |
|---|---|---|
com.aiharataketo.secretary.plist |
~/Library/LaunchAgents/ |
ログイン時にstart-secretary.shを実行 |
start-secretary.sh |
~/.claude/secretary/ |
tmuxセッションの存在確認 + 作成 |
run-claude.sh |
~/.claude/secretary/ |
Claude起動 + クラッシュ時の再起動ループ |
claude \
--name "secretary" \
--channels plugin:telegram@claude-plugins-official \
--allowedTools "Read,Write,Glob,Grep,Bash,mcp__plugin_telegram_telegram__reply,mcp__plugin_telegram_telegram__react,mcp__plugin_telegram_telegram__edit_message,mcp__plugin_telegram_telegram__download_attachment"
--channels plugin:telegram@claude-plugins-official でTelegramチャンネルを有効化する。このフラグがないとTelegramメッセージを受信できない。
--dangerously-skip-permissions は使わない。 --allowedTools で必要なツールだけ許可する方が安全。秘書の安全弁は propose モードであり、OS レベルの権限バイパスではない。
Write が必要。 jobs.json の更新に使う。Agent / WebSearch / WebFetch は秘書の責務外なので含めない。
--permission-mode auto は使えない。 Claude Max プランでは非対応(Team plan 限定)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.aiharataketo.secretary</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/aiharataketo/.claude/secretary/start-secretary.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/aiharataketo/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/aiharataketo</string>
<key>LANG</key>
<string>ja_JP.UTF-8</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/aiharataketo/.claude/logs/secretary-launchagent.log</string>
<key>StandardErrorPath</key>
<string>/Users/aiharataketo/.claude/logs/secretary-launchagent.err.log</string>
<key>ThrottleInterval</key>
<integer>30</integer>
</dict>
</plist>
KeepAlive=false の理由: スクリプトはtmuxセッションを作って即終了する設計。KeepAlive=true だと「終了のたびに再起動」で無限ループになる。tmuxが独立デーモンとして動くので不要。
| 条件 | 判定 | アクション |
|---|---|---|
| 正常終了(30秒以上稼働) | exit後 | 5秒待って再起動 |
| クラッシュ(30秒未満で終了) | restart_count++ | 10秒待って再起動 |
| 連続クラッシュ5回 | restart_count >= 5 | 30分待機後にリセット |
Claude CodeはmacOS Login Keychainに認証を保存する。LaunchAgentはGUIログイン後に起動するため通常はkeychainにアクセスできる。
| 状況 | keychainアクセス | 備考 |
|---|---|---|
| 通常ログイン後 | OK | 問題なし |
| スリープ→復帰 | OK | tmuxセッション生存 |
| 再起動後ログイン前 | LaunchAgent未起動 | 設計通り(RunAtLoad) |
フォールバック: keychainが問題になった場合は claude setup-token で生成したトークンを CLAUDE_CODE_OAUTH_TOKEN 環境変数としてplistの EnvironmentVariables に追加する。
run-claude.sh のwhileループがClaude終了を検知して再起動する# セットアップ
chmod +x ~/.claude/secretary/start-secretary.sh ~/.claude/secretary/run-claude.sh
launchctl load ~/Library/LaunchAgents/com.aiharataketo.secretary.plist
# 状態確認
launchctl list | grep secretary
tmux has-session -t secretary && echo "alive" || echo "dead"
# 手動再起動
tmux kill-session -t secretary
bash ~/.claude/secretary/start-secretary.sh
# ログ確認
tail -f ~/.claude/logs/secretary.log
| 課題 | ステータス | 備考 |
|---|---|---|
| レジストリの自動登録 | 未実装 | 手動で十分なら手動のまま(YAGNI) |
| 設計済み(v4) | LaunchAgent + while trueループ。上記「常駐化」参照 | |
| 検証済み | nuchiが実動作確認。Telegram上でAllow/Denyボタンが表示される | |
| recall の依存境界 | 要明記 | nuchi-skills のスキル経由。常設CLIではない |
| スリープ復帰後のTelegram再接続 | 要検証 | 自動再接続するか、Claude再起動が必要か |
| 秘書CLAUDE.md | 未設計 | 秘書セッション固有のルール(Phase 0で検証しながら作成) |
| Telegramからの画像読み取り | 要検証 | image_path属性 → Readツール → マルチモーダル認識の経路 |
registry.json 導入済みjobs.json 導入。アクティブジョブ1件制限(PENDING/SENT/BLOCKED いずれかが存在する間は新規dispatch禁止)| 指摘 | 却下理由 |
|---|---|
per-session の allowed_chat_ids / allowed_tools_profile |
個人ツール。ユーザーは1人 |
list/claim/lock/abort/ack/inspect API |
現時点で不要。必要になったら追加 |
| 5フェーズ分割 | 4フェーズで十分 |
| 状態遷移図の明記 | 実装時に決める |
| deny-by-default のper-session権限境界 | propose モードが安全弁。個人ツールにエンタープライズ権限は不要 |
ジョブに awaiting_reply_kind と blocked_reason を追加。Telegram返信をどの待機状態に結びつけるかを永続化する。
{
"job_id": "j-20260326-001",
"message": "課金モデル変更して",
"target_session": "line-call-reminder",
"status": "BLOCKED",
"chat_id": "7789180125",
"status_message_id": "100",
"awaiting_reply_kind": "permission_modal",
"blocked_reason": "Do you want to run npm install?",
"created_at": "2026-03-26T22:35:00Z"
}
awaiting_reply_kind: null / propose_approval / permission_modal / ask_user_question / clarify
status_message_id: 進捗報告用のTelegramメッセージID(v4「進捗報告プロトコル」参照)
dispatch前に tmux has-session -t SESSION_NAME を実行。存在しなければdispatchせず、Telegramに「セッションが見つからない」と返す。tmux.pyの変更は不要。
| status | awaiting_reply_kind | 意味 | 遷移先 |
|---|---|---|---|
| PENDING | clarify | セッション特定待ち | → PENDING(propose) or キャンセル |
| PENDING | propose_approval | ユーザー承認待ち | → SENT or キャンセル |
| SENT | null | 実行中 | → DONE / BLOCKED / FAILED |
| BLOCKED | permission_modal | permission応答待ち | → SENT(再待機) |
| BLOCKED | ask_user_question | 質問応答待ち | → SENT(再待機) |
| DONE | null | 完了 | 終端 |
| FAILED | null | 失敗(timeout/session_gone) | 終端 |
Phase 1 は clarify/propose の精度検証が目的。秘書が落ちてもpending proposalは「もう一回送って」で済む。本格的なジョブ永続化はPhase 2から。
tmux.py は「新規セッション作成 + テスト」を主導線として設計されているが、秘書では既存セッションへの send/wait/read のみ使用する。setup は使わない。
未解決の対話中ジョブは常に1件まで。 PENDING または BLOCKED のジョブが存在する間は、新しいジョブをdispatchしない。新しい指示が来た場合は「今 [セッション名] で作業中。終わってから対応するわ」と返す。
これにより、Telegram返信がどのジョブに対するものか曖昧にならない。Phase 3 で並列化が必要になった時点で reply_to_message_id を導入する。
--channels フラグ--channels plugin:telegram@claude-plugins-official で Telegram チャンネルを有効化する。このフラグを指定すると、セッションが Telegram メッセージを受信できるようになる。
claude --channels plugin:telegram@claude-plugins-official
注意: v4初稿では「--channels は存在しない」と記載していたが誤り。実地検証で動作確認済み(2026-03-27)。
3つの記述が矛盾していた:
| ソース | 方式 | 正しさ |
|---|---|---|
| MEMORY.md | Enter → Escape → sleep → Enter |
❌ 古い(手動tmux send-keys時代の記述) |
| tmux-claude-debugging スキル | Enter Escape → sleep → Enter |
⚠️ 動くがAskUserQuestion状態で問題 |
tmux.py send |
send-keys -l PROMPT + Enter(Escapeなし) |
✅ 正 |
tmux.pyのコメントに理由が明記されている:
AskUserQuestion state consumes Escape as "cancel question", preventing the prompt from being sent.
秘書は tmux.py の send サブコマンドのみ使用する。 直接 tmux send-keys を使ってはならない。
nuchiが実動作確認済み。TelegramプラグインはAllow/Denyボタンをインラインで表示する。設計書の「要検証」ステータスを解消。
--permission-mode auto は Claude Max プランでは使用不可(Team plan 限定)。秘書の権限管理は --allowedTools で必要なツールのみ許可する方式に確定。
Telegramから送信された画像は以下の経路で読める:
<channel ... image_path="/path/to/image.jpg"> → Read(image_path) → マルチモーダル認識
秘書がスクリーンショットを受け取って画面状態を判断するユースケースが可能。
秘書は1つのステータスメッセージを edit_message で更新し続け、ユーザーのアクションが必要な時だけ新規 reply(プッシュ通知あり)を送る。
| ツール | 通知 | 使うタイミング |
|---|---|---|
reply → ステータスメッセージ作成 |
あり | ジョブ開始時に1回だけ |
edit_message → ステータス更新 |
なし | 進捗が変わるたびに(何度でも) |
reply → 新規メッセージ |
あり | ユーザーのアクションが必要な時(承認待ち・質問・完了・失敗) |
User: 「コールリマインダーの課金変えたい」
Secretary: reply →
"🔍 セッション特定中..." ← msg_id: 100 を記録
Secretary: edit_message(100) →
"🔍 line-call-reminder を特定
📋 提案を作成中..."
Secretary: edit_message(100) →
"🔍 line-call-reminder を特定
📋 提案: 「課金モデルをサブスク制に変更」を送る
⏳ 承認待ち"
User: 「OK」
Secretary: edit_message(100) →
"🔍 line-call-reminder を特定
📋 提案: 承認済み
⚡ dispatch中..."
Secretary: edit_message(100) →
"🔍 line-call-reminder を特定
📋 提案: 承認済み
⚡ 実行中..."
--- ここで完了またはブロック ---
【完了の場合】
Secretary: edit_message(100) →
"🔍 line-call-reminder を特定
📋 提案: 承認済み
✅ 完了"
Secretary: reply(新規) → ← プッシュ通知!
"✅ 課金モデルの変更が完了したわ。
変更内容: ..."
【ブロックの場合(permission_modal)】
Secretary: edit_message(100) →
"🔍 line-call-reminder を特定
📋 提案: 承認済み
⚠️ 権限の承認待ち"
Secretary: reply(新規) → ← プッシュ通知!
"⚠️ line-call-reminder が権限を求めてるわ:
「npm install を実行していい?」
OK / NG で返して"
| 絵文字 | 意味 |
|---|---|
| 🔍 | セッション特定中 / 情報収集中 |
| 📋 | 提案作成 / 承認待ち |
| ⚡ | dispatch / 実行中 |
| ⏳ | ユーザー応答待ち |
| ✅ | 完了 |
| ⚠️ | ブロック(要アクション) |
| ❌ | 失敗 |
ステータスメッセージの message_id をジョブに紐づけて永続化する。秘書が再起動しても、どのメッセージを edit_message すればよいか分かる。
{
"job_id": "j-20260326-001",
"message": "課金モデル変更して",
"target_session": "line-call-reminder",
"status": "SENT",
"chat_id": "7789180125",
"status_message_id": "100",
"awaiting_reply_kind": null,
"blocked_reason": null,
"created_at": "2026-03-26T22:35:00Z"
}
status_message_id: ステータスメッセージのTelegram message_id。edit_message の対象。ジョブ完了/失敗で不要になるが、履歴として残す。