---
title: "Claude Codeに頼む手作業を、依頼箱で自動にした"
slug: claude-code-api-key-billing
date: 2026-10-27
day: 390
meta_description: "設計はClaude、実装はClaude Codeに分け、依頼文を毎回手で貼っていた。フォルダに置くとMacの見張り係がClaude Codeを画面なしで動かし報告を戻す形にした。途中、自動の実行だけAPIの従量課金になっていたのも直した。"
tags: [Claude Code, Claude, 自動化, AI活用, Mac, ターミナル, 依頼箱]
ai_consult: true
series: "ひとり研究所の裏側"
---

ロボットのシミュレーションを進めるとき、作業を2つの AI に分けています。設計と指示文を書くのは Claude（チャットで話す AI）。Mac の上でコードを書いて動かすのは Claude Code（Mac のターミナルで動く、コードを書く AI）です。この2つの間で依頼文を運んでいたのは、人の手でした。Claude が書いた依頼文を開いてコピーし、Claude Code の画面に貼り、終わったら結果をまた Claude に伝える。配膳ロボットのシミュレーションだけで、10月20日から26日の7日間に、依頼文が21本、Claude Code からの報告が43本、この往復を通っています。

> 今はUnityや実機の作業をclaude codeに頻繁に手動で頼んでいますが、それが自動化できないか（重要な指示だけは手動はいいけど、それ以外のところ）

そこで「依頼箱」を作りました。iCloud のフォルダに Claude が依頼文を置くと、Mac で待っている小さなプログラム（見張り係）がそれを見つけ、Claude Code を画面なしで動かします。Claude Code の報告は同じフォルダに戻り、Mac に通知が出ます。人が手を入れるのは、決めてほしいことが出たときと、実機を動かす前だけです。

作る途中で、予想していなかったことが2つありました。1つは、据え付けを Claude Code に頼むと、「別の Claude を起動して無人で動かす」操作だけが、Claude Code 自身の安全の仕組みに止められたこと。もう1つはお金です。**最初に自動で動いた Claude Code は、契約している定額のプランではなく、APIキーの従量課金で払われていました。** ふだん手で使う Claude Code は定額のプランなのに、画面なしで起動したときだけ、Mac に設定されていた APIキー（使った分だけ払う契約の鍵）が優先されていたのです。1回分は約16円（Claude Code の見積もりで0.0997ドル。1ドル＝158円）。見張り係が Claude Code を起動するときだけ APIキーを外すように直し、2回目は定額のプランの枠で動きました。

| やったこと | なぜ | 結果 |
|---|---|---|
| Claude が Mac のどこに届くかを確かめた | どこまで自動にできるかを決めるため | 届くのは選んだ2つのフォルダだけ。ターミナルのアプリには文字を打てない |
| 依頼箱と見張り係を作った | 手で貼る往復をなくすため | 依頼を置いてから報告が戻るまで約20〜30秒（疎通確認2回） |
| 自動で回す場所を1つに絞り、危ない操作を拒否にした | 無人で動くので、範囲を仕組みで縛るため | 回すのはサービスロボット導入シミュレーションの Unity のフォルダだけ |
| 偽の Claude Code で9つの場合を確かめた | 本物で失敗すると、お金と時間がかかるため | 時間切れのとき見張り係が止まらなくなる穴を見つけて直した |
| APIキーを外し、claude.ai のコネクタも止めた | 従量課金を避け、無人の作業が Notion などに書き込めないようにするため | 警告が消え、コネクタは0個になった |
| Mac にログインしたら自動で起動する形にした | 再起動のたびにコマンドを貼らないため | 次の再起動で確かめる |

1回目の疎通確認（つながるかを確かめるだけの依頼）では、Claude Code が夜のシミュレーションの判定を読み、件数の表と本文が食い違っているのを見つけてきました。判定のスクリプトの不具合です。まだ本番の依頼は1本も流していません。依頼箱に最初に流す本番の依頼は、この不具合の直しにする予定です。


## 作業ログ ― Mac で届く範囲を確かめてから、依頼箱が再起動のあとも動くようにするまで

ここから下は作業の記録です。時間・件数・費用は、ログとファイルから読んだ値です。依頼箱で手作業がどれだけ減るかは、まだ測っていない見込みで、何で確かめるかを添えています。

### Claude が Mac で使えるのは、選んだ2つのフォルダの中のシェルだけだった

この作業は、Claude のデスクトップアプリから Mac につないだ会話で進めました。最初に、Claude がその Mac で何をできるかを確かめています。

Claude が Mac で使えるシェル（コマンドを打つ画面）は、Mac の中で動く小さな Linux の上にあります。そこから見えるのは、会話を始めるときに選んだ2つのフォルダ（iCloud の PixelstoMotors と、外付けの SSD の KIOXIA）だけです。

```
$ uname -a; ls $HOME/mnt/; which python3 git node
Linux claude 6.8.0-138-generic #138~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug  7 13:34:27 UTC  aarch64 aarch64 aarch64 GNU/Linux
KIOXIA
PixelstoMotors
/usr/bin/python3
/usr/bin/git
/usr/bin/node
```

Mac の画面を見て操作する機能もありますが、ターミナルのアプリは「クリックだけ」の扱いで、文字を打つことも貼り付けることもできない決まりです。つまり Claude から直接は、Unity・実機・`~/r2r2r-scripts`（実機のコード）・Mac の Claude Code のどれにも届きません。届くのはフォルダだけなので、フォルダを受け渡しの場所にする形にしました。

### 手で運んでいた依頼文は、7日間で21本、報告は43本だった

配膳ロボットのシミュレーション（Unity で作ったファミレスで、配膳ロボットと店員を動かす試算）の作業フォルダには、Claude Code あての依頼文と、Claude Code からの報告が日付つきで残っています。

```
$ ls -l --time-style=+%m-%d claude_code_request_*.md | awk '{print $6, $7}' | sort
10-20 claude_code_request_v0.md
10-20 claude_code_request_v1_store.md
10-20 claude_code_request_v2_guests.md
10-21 claude_code_request_v3_fixes.md
10-22 claude_code_request_v4_json_store.md
10-22 claude_code_request_v5_align.md
10-22 claude_code_request_v5b_routes.md
10-23 claude_code_request_v6_yield.md
10-23 claude_code_request_v6b_yield_only_in_path.md
10-23 claude_code_request_v7_stageB.md
10-23 claude_code_request_v7b_stageB_resume.md
10-23 claude_code_request_v8_dish_arrival.md
10-24 claude_code_request_v8b_timeout_place_rng.md
10-24 claude_code_request_v8b_timeout_rng.md
10-24 claude_code_request_v9_map_planner.md
10-25 claude_code_request_v10_valve_v28.md
10-25 claude_code_request_v10b_stageB_timeline.md
10-25 claude_code_request_v11_project_split.md
10-25 claude_code_request_v12_night_runner.md
10-25 claude_code_request_v13_pass_by_and_clock.md
10-26 claude_code_request_v14_ledger_fixes.md

$ ls claude_code_reports | cut -c1-8 | sort | uniq -c
      6 20261020
      9 20261021
      9 20261022
      2 20261023
      1 20261024
      6 20261025
      7 20261026
      3 20261027
```

夜のあいだに Unity を回す仕組み（画面なしの実行ファイルと、並べた条件を順に回す実行係）は、10月25日にできています。残っていた手作業は、依頼文を Claude Code に渡すところと、報告を Claude に戻すところでした。

### 依頼箱はフォルダ6つで、依頼の頭に作業フォルダと上限時間を書く

依頼箱は iCloud の `PixelstoMotors/依頼箱/` です。Claude は会話の中からこのフォルダに書けて、人は iPhone からでも中を見られます。

| フォルダ | 中身 |
|---|---|
| 新規 | Claude が依頼を置く。見張り係が1件ずつ自動で実行する |
| 確認待ち | 人の確認が要る依頼。読んで良ければ「新規」へ移すと実行される |
| 実行中 | いま Claude Code が作業している依頼 |
| 済み／失敗 | 終わった依頼 |
| 結果 | Claude Code の報告（.md）と元の出力（.json） |
| _仕組み | 見張り係の元のファイル。据え付け先は Mac の `~/r2r2r-scripts/inbox/` |

依頼文の頭には、どのフォルダで作業するかと、何分で打ち切るかだけを書きます。

```
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 120
---
（依頼の本文）
```

自動で回してよい作業フォルダは、設定ファイルの `allowed_dirs` に書いたものだけにしました。今はサービスロボット導入シミュレーションの Unity のプロジェクト（`ServiceSim`）だけです。ロボットアームの Unity や実機のコードのフォルダの依頼が来ても、実行せずに「失敗」へ回します。このプロジェクトでは、関節や姿勢を変えるコードは、設計 → 実装 → 外部のレビュー → 人が実機で試す、の順を飛ばさない決まりがあります。依頼箱で自動にするのは、実装とレビューまで。実機に流す前は「確認待ち」で止めます。

- ねらい：Claude と Claude Code の間で、人が依頼文と報告を運ぶ往復をなくす
- 変わったこと：Claude が「新規」に依頼を置けば、見張り係が Claude Code を起動し、報告が「結果」に戻る。人が見るのは、報告の「決めてほしいこと」と「確認待ち」だけ
- 見込める効果：依頼1本あたりの貼る・戻すの手作業がなくなる（7日間で21本の依頼・43本の報告の分）。依頼ごとに新しい短い会話で動くので、長い会話で頼み続けるより、定額のプランの枠の減りも少ない見込み。実際に減ったかは、依頼箱の `記録.log` の本数と、プランの使用量の表示で確かめる

【図解】LOG390_inbox_flow.html

### 見張り係は Claude Code を画面なしで起動し、承認を人に聞かない

見張り係（`inbox_runner.py`）は、30秒ごとに「新規」を見て、依頼があれば Claude Code を次の形で起動します。

```
claude -p "<依頼文>" --output-format json --permission-mode auto --permission-prompts none --settings settings.json
```

- `-p`：画面を開かずに1回だけ動かし、終わったら返事を出して閉じる
- `--output-format json`：返事を、報告の文・会話の番号・止められた操作の一覧などが入った形で受け取る
- `--permission-mode auto`：ファイルの書き換えやコマンドを、人に聞く代わりに、Claude Code の中の判定の仕組みが1つずつ見て通すか決める
- `--permission-prompts none`：人に聞かないと進めない操作は、待たずに断る。夜中に承認待ちで止まらないようにするため
- `--settings settings.json`：必ず断る操作の一覧。`git clean`・`git add .`・強制の push・`rm -rf`・`sudo` を入れた。`git clean` は、以前 Unity のシーンを消した事故があってから使わない決まり

依頼文の前には、見張り係が毎回同じ前置きを付けます。人は見ていないこと、範囲の外は変えないこと、方針や数値の決め方・公開・実機を動かすことに当たったら手を止めて「決めてほしいこと」に書くこと、報告は日本語で決まった形にすること、の4つです。前置きの全文は、下のコードの `PREAMBLE` にあります。

### 時間切れの確かめで、見張り係が止まらなくなる穴が見つかった

本物の Claude Code で試す前に、決まった返事を返すだけの偽の Claude Code を作って確かめました。偽物は、依頼文の言葉によって、すぐ返す・5分眠る・子のプロセス（裏で動き続ける別のプログラム）を残して終わる、などを切り替えます。

最初の版で「5分眠る」依頼を上限1分で流したところ、見張り係が戻ってきませんでした。確かめのコマンドは2分30秒で打ち切られています。最初の版は、Claude Code の出力を管（パイプ）で受け取り、時間切れのときは Claude Code に止まる合図を送ってから、管が閉じるのを待っていました。

```python
    p = subprocess.Popen(cmd, cwd=str(workdir), stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE, text=True, encoding="utf-8")
    timed_out = False
    try:
        out, err = p.communicate(timeout=limit_min * 60)
    except subprocess.TimeoutExpired:
        timed_out = True
        # 先に SIGINT（その回の作業を終わらせる）→ だめなら SIGTERM → 最後に kill
        for sig, wait in ((signal.SIGINT, 120), (signal.SIGTERM, 60)):
            p.send_signal(sig)
            try:
                out, err = p.communicate(timeout=wait)
                break
            except subprocess.TimeoutExpired:
                continue
        else:
            p.kill()
            out, err = p.communicate()
```

偽物は眠るために `sleep` という子のプロセスを起動していて、その子が管の口を持ったままでした。親が止まっても子が管を閉じないので、見張り係は管が閉じるのを待ち続けます。本物の Claude Code も、ビルドや Unity の実行を裏で動かしたまま終わることがあるので、同じことが起きえます。

- ねらい：時間切れや、子のプロセスが残ったときでも、見張り係が次の依頼へ進めるようにする
- 変わったこと：出力を管ではなく一時ファイルで受け取り、Claude Code を自分専用のプロセスのまとまりで起動する。時間切れのときは、そのまとまり全体に、止まる合図 → 終われの合図 → 強制終了の順で送る。時間内に終わったときは、残った子のプロセスに手を出さない（夜の実行係を裏で起動して終わる依頼もあるため）
- 見込める効果：直す前は、上限1分の依頼で2分30秒たっても戻らなかった（打ち切られるまで待ち続けた）。直したあとは、同じ依頼が61秒で「時間切れ」として報告に残り、次の依頼へ進んだ（下の確かめの出力）

### 偽の Claude Code で9つの場合を確かめた

確かめたのは次の9つです。設定の確かめ・前回作業の途中で止まった依頼・正常に終わる依頼・自動で回さない場所の依頼・頭の項目がない依頼・子のプロセスを残して終わる依頼・時間切れ・APIキーを外さない設定にしたとき・見張り係を2つ起動したとき。偽物には、渡された環境変数を書き出させ、APIキーが外れているかも見ています（APIキーの話は下の節）。確かめの APIキーは `dummy-not-a-key` という偽の文字です。

#### test_inbox_runner.sh（確かめのスクリプト）

```bash
#!/bin/bash
# 依頼箱の見張り係を、偽の claude で確かめる（Linux の作業環境で動かす）
# 使い方: bash test_inbox_runner.sh <inbox_runner.py があるフォルダ>
set -u
SRC="$(cd "$1" && pwd)"
T="$(mktemp -d)"
mkdir -p "$T/run" "$T/box/新規" "$T/work" "$T/other"
cp "$SRC/inbox_runner.py" "$SRC/settings.json" "$T/run/"
export FAKE_LOG="$T/fake.log"

# 偽の claude：渡された環境変数を記録し、依頼文の言葉で振る舞いを変える
cat > "$T/fakeclaude" <<'EOF'
#!/bin/bash
if [ "$1" = "--version" ]; then echo "2.1.311 (fake)"; exit 0; fi
echo "key=${ANTHROPIC_API_KEY:-なし} mcp=${ENABLE_CLAUDEAI_MCP_SERVERS:-なし}" > "$FAKE_LOG"
[ -n "${ANTHROPIC_API_KEY:-}" ] && echo "⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another auth source is set" >&2
if grep -q "少し待つ" <<<"$2"; then sleep 5; fi
if grep -q "置いていく" <<<"$2"; then nohup sleep 200 >/dev/null 2>&1 & fi
if grep -q "眠る" <<<"$2"; then
  trap 'echo "{\"result\":\"中断\",\"is_error\":true}"; exit 130' INT
  sleep 300 & wait
fi
echo '{"type":"result","is_error":false,"result":"やったこと：確認\n決めてほしいこと：なし","session_id":"abc-123","permission_denials":[{"tool_name":"Bash","tool_input":{"command":"git clean -fd"}}]}'
EOF
chmod +x "$T/fakeclaude"

python3 - "$SRC/config.json" "$T" <<'EOF'
import json, sys
c = json.load(open(sys.argv[1], encoding="utf-8"))
t = sys.argv[2]
c.update(inbox=f"{t}/box", allowed_dirs=[f"{t}/work"], claude=f"{t}/fakeclaude",
         settle_seconds=0, default_limit_minutes=1)
json.dump(c, open(f"{t}/run/config.json", "w", encoding="utf-8"), ensure_ascii=False)
EOF

req() { printf -- '---\n作業フォルダ: %s\n%s---\n%s\n' "$2" "$3" "$4" > "$T/box/新規/$1.md"; }

echo "== 確かめ1：設定の確かめ（APIキーが設定された窓）"
ANTHROPIC_API_KEY=dummy-not-a-key python3 "$T/run/inbox_runner.py" --check

echo; echo "== 確かめ2：正常／自動で回さない場所／頭の項目なし／子のプロセスを残す／時間切れ"
req 01_ok "$T/work" "" "疎通確認です"
req 02_ng "$T/other" "" "だめな場所"
printf 'ヘッダなし\n' > "$T/box/新規/03_nohead.md"
req 04_leave "$T/work" "" "置いていく依頼"
req 05_sleep "$T/work" "上限時間: 1
" "眠る依頼"
mkdir -p "$T/box/実行中"; echo "x" > "$T/box/実行中/00_stuck.md"
START=$(date +%s)
ANTHROPIC_API_KEY=dummy-not-a-key timeout 200 python3 "$T/run/inbox_runner.py" --once
echo "かかった秒数: $(( $(date +%s) - START ))"
echo "偽の claude が受け取った環境: $(cat "$FAKE_LOG")"

echo; echo "== フォルダの中身"
(cd "$T/box" && find . -type f | sort)
for f in "$T"/box/結果/*.md; do echo; echo "---- $(basename "$f")"; sed -n '1,3p;/## Claude Code の報告/,$p' "$f"; done

echo; echo "== 確かめ3：APIキーを外さない設定にしたとき、報告に★が付くか"
python3 - "$T/run/config.json" <<'EOF'
import json, sys
c = json.load(open(sys.argv[1], encoding="utf-8")); c["drop_env"] = []
json.dump(c, open(sys.argv[1], "w", encoding="utf-8"), ensure_ascii=False)
EOF
req 06_key "$T/work" "" "キーの確かめ"
ANTHROPIC_API_KEY=dummy-not-a-key python3 "$T/run/inbox_runner.py" --once
echo "偽の claude が受け取った環境: $(cat "$FAKE_LOG")"
grep "★" "$T"/box/結果/06_key_*.md

echo; echo "== 確かめ4：2つ目の見張り係は起動しない"
req 07_wait "$T/work" "" "少し待つ依頼"
python3 "$T/run/inbox_runner.py" --once & P=$!
sleep 1; python3 "$T/run/inbox_runner.py" --once; echo "2つ目の終了コード: $?"; wait $P
pkill -f "sleep 200" 2>/dev/null; pkill -f "sleep 300" 2>/dev/null
rm -rf "$T"
```

#### test_output.txt（確かめの出力）

```
== 確かめ1：設定の確かめ（APIキーが設定された窓）
依頼箱: /tmp/tmp.WU9xt3sglO/box あり
自動で回してよい場所: /tmp/tmp.WU9xt3sglO/work あり
claude: /tmp/tmp.WU9xt3sglO/fakeclaude
claude の版: 2.1.311 (fake)
承認の扱い: auto ／ 拒否の決まり: /tmp/tmp.WU9xt3sglO/run/settings.json
外す環境変数: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN ／ うち今この窓で設定されているもの: ANTHROPIC_API_KEY

== 確かめ2：正常／自動で回さない場所／頭の項目なし／子のプロセスを残す／時間切れ
2026-10-27T08:40:02 見張り係を起動（1回だけ）
2026-10-27T08:40:02 中断されていた依頼を失敗へ 00_stuck.md
2026-10-27T08:40:02 開始 01_ok.md
2026-10-27T08:40:03 完了 01_ok.md → 結果/01_ok_20261027_0840.md
2026-10-27T08:40:03 開始 02_ng.md
2026-10-27T08:40:03 失敗 02_ng.md → 結果/02_ng_20261027_0840.md
2026-10-27T08:40:03 開始 03_nohead.md
2026-10-27T08:40:03 失敗 03_nohead.md → 結果/03_nohead_20261027_0840.md
2026-10-27T08:40:03 開始 04_leave.md
2026-10-27T08:40:03 完了 04_leave.md → 結果/04_leave_20261027_0840.md
2026-10-27T08:40:03 開始 05_sleep.md
2026-10-27T08:41:03 時間切れ（途中で止めました） 05_sleep.md → 結果/05_sleep_20261027_0840.md
かかった秒数: 61
偽の claude が受け取った環境: key=なし mcp=false

== フォルダの中身
./失敗/00_stuck.md
./失敗/02_ng.md
./失敗/03_nohead.md
./失敗/05_sleep.md
./済み/01_ok.md
./済み/04_leave.md
./結果/00_stuck_20261027_0840.md
./結果/01_ok_20261027_0840.json
./結果/01_ok_20261027_0840.md
./結果/02_ng_20261027_0840.md
./結果/03_nohead_20261027_0840.md
./結果/04_leave_20261027_0840.json
./結果/04_leave_20261027_0840.md
./結果/05_sleep_20261027_0840.json
./結果/05_sleep_20261027_0840.md
./記録.log

---- 00_stuck_20261027_0840.md
# 結果：00_stuck

- 状態：失敗
## Claude Code の報告

見張り係が止まった（Mac の再起動など）ため、この依頼は途中で終わっています。途中まで変わったファイルがあるかもしれません。やり直すかどうかは、確かめてから決めます。

---- 01_ok_20261027_0840.md
# 結果：01_ok

- 状態：完了
## Claude Code の報告

やったこと：確認
決めてほしいこと：なし

## 止められた操作

- {"tool_name": "Bash", "tool_input": {"command": "git clean -fd"}}

---- 02_ng_20261027_0840.md
# 結果：02_ng

- 状態：失敗
## Claude Code の報告

作業フォルダ /tmp/tmp.WU9xt3sglO/other は自動で回してよい場所（config.json の allowed_dirs）に入っていません。実行していません。手で頼むか、allowed_dirs に足すかを決めてください。

---- 03_nohead_20261027_0840.md
# 結果：03_nohead

- 状態：失敗
## Claude Code の報告

依頼の頭に「作業フォルダ:」がありません。実行していません。

---- 04_leave_20261027_0840.md
# 結果：04_leave

- 状態：完了
## Claude Code の報告

やったこと：確認
決めてほしいこと：なし

## 止められた操作

- {"tool_name": "Bash", "tool_input": {"command": "git clean -fd"}}

---- 05_sleep_20261027_0840.md
# 結果：05_sleep

- 状態：時間切れ（途中で止めました）
## Claude Code の報告

中断

== 確かめ3：APIキーを外さない設定にしたとき、報告に★が付くか
2026-10-27T08:41:03 見張り係を起動（1回だけ）
2026-10-27T08:41:03 開始 06_key.md
2026-10-27T08:41:03 完了 06_key.md → 結果/06_key_20261027_0841.md
偽の claude が受け取った環境: key=dummy-not-a-key mcp=false
- ★ APIキーで動いた可能性があります（プランの利用枠ではなく従量課金）。_stderr.txt を見てください

== 確かめ4：2つ目の見張り係は起動しない
2026-10-27T08:41:03 見張り係を起動（1回だけ）
2026-10-27T08:41:03 開始 07_wait.md
見張り係はもう動いています（2つ目は起動しません）
2つ目の終了コード: 1
2026-10-27T08:41:08 完了 07_wait.md → 結果/07_wait_20261027_0841.md
```

### 据え付けを頼んだ Claude Code は、別の Claude の起動を自分の安全の仕組みで止めた

見張り係を Mac に入れる作業は、Claude Code に1回だけ手で頼みました。頼んだのは5つの手順です。ファイルを `~/r2r2r-scripts/inbox/` にコピーして commit する、Claude Code の版を確かめる、見張り係の設定を確かめる、承認の扱い「auto」が使えるかを試す、見張り係を新しいターミナルの窓で起動する。

1〜3は通りました。Claude Code の版は 2.1.220 で、画面なしで承認を人に聞かない指定（`--permission-prompts`）に要る 2.1.259 より古かったので、Claude Code 自身が 2.1.311 に上げています。

4と5は止まりました。Claude Code の窓の中から `claude -p` を起動する操作も、ターミナルの窓を開いて見張り係を起動する操作も、Claude Code の判定の仕組みに「Blocked by classifier」で断られています。**Claude Code が別の Claude を起動して無人で動かす操作は、Claude Code の中から勝手にはできない、ということです。**Claude Code は3回目を試さずに止まり、人が手で打つコマンドを報告に書いてきました。

この報告を受けて、4は飛ばしました。疎通確認の依頼そのものが同じ確かめになり、auto が使えなければ報告にそう出るからです。5は、人がふつうのターミナルで1行打つ形にしました。

```
python3 ~/r2r2r-scripts/inbox/inbox_runner.py
```

#### 据え付け_20261027.md（Claude Code の報告）

````markdown
# 据え付け報告：依頼箱の見張り係（2026-10-27）

状態：**手順1〜3は完了。手順4・5は未実施**（この Claude Code の窓からは claude の起動と
ターミナル操作が承認の仕組みで止められるため。下の「決めてほしいこと」を見てください）

## 手順1：ファイルのコピーと commit — 完了

元のファイル3つを `~/r2r2r-scripts/inbox/` にコピーしました。

- `inbox_runner.py`（12,692 バイト）
- `config.json`
- `settings.json`
- `.gitignore`（新しく作成。中身は `.runner.lock` の1行だけ）

4つを個別に指定して add し、commit しました（`git add .`・`git clean` は使っていません）。
`~/r2r2r-scripts` には他に多数の未追跡・変更済みファイルがありますが、どれも触っていません。

- commit の番号：`636f374b0dd64e683f2aea031a59ca1004929d26`（短い番号 `636f374`）
- 件名：依頼箱の見張り係を据え付け（inbox_runner）

## 手順2：claude の版 — 完了（上げました）

- 据え付け前：**2.1.220**（2.1.259 より古い）
- `claude update` を実行：2.1.220 → **2.1.311**
- 場所：`/Users/shimazueiichi/.npm-global/bin/claude`

## 手順3：`--check` — 完了（全部「あり」）

```
依頼箱: /Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/依頼箱 あり
自動で回してよい場所: /Users/shimazueiichi/Robotics_UX/ServiceSim あり
claude: /Users/shimazueiichi/.npm-global/bin/claude
claude の版: 2.1.311 (Claude Code)
承認の扱い: auto ／ 拒否の決まり: /Users/shimazueiichi/r2r2r-scripts/inbox/settings.json
```

終了コードは 0。claude は見つかったので、`config.json` の `"claude"` は `"claude"` のまま
変えていません。

## 手順4：承認の扱い「auto」が使えるか — 未実施

指示された確認の命令を2通りの出し方で試しましたが、どちらもこの窓の承認の仕組み
（auto mode classifier）に止められました。

1. ターミナルで直に `claude -p ... --permission-mode auto --permission-prompts none`
   → 「Blocked by classifier」で拒否
2. 手順5と同じ `osascript` でターミナルの窓に出す形
   → 同じく「Blocked by classifier」で拒否

これは**この Claude Code の窓の制限**であって、見張り係が使う `auto` が有効かどうかの
答えにはなっていません。見張り係は別のプロセスとして動くので、エイイチが手で確かめる
必要があります。

`config.json` は**変えていません**（`"permission_mode": "auto"` のまま）。

## 手順5：見張り係の起動 — 未実施

手順4の答えが出ていないため、起動していません。`.runner.lock` も作られていません。

## 疎通確認の依頼について（参考）

`依頼箱/新規/20261027_01_疎通確認.md` はそのまま置いてあります。見張り係が起動すれば
30 秒以内に拾います。その依頼が読む `/Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/`
は今マウントされていて、一番新しい日付は `20261027` です。

## 変えたファイル

- 新規作成：`~/r2r2r-scripts/inbox/inbox_runner.py`・`config.json`・`settings.json`・`.gitignore`
- claude 本体を 2.1.220 → 2.1.311 に更新
- Assets・ServiceSim のプロジェクトは何も変えていません

## 決めてほしいこと

手順4と5を、エイイチが手で（またはこの窓で `!` を頭に付けて）実行してください。

1. auto が使えるかの確認：
   ```
   cd /Users/shimazueiichi/Robotics_UX/ServiceSim && claude -p "OKとだけ返して" --output-format json --permission-mode auto --permission-prompts none --settings ~/r2r2r-scripts/inbox/settings.json
   ```
   - うまく返れば `config.json` はそのままで次へ。
   - auto が無効・選べないという誤りが出たら、`config.json` を
     `"permission_mode": "acceptEdits"`、
     `"extra_args": ["--allowedTools", "Bash,Read,Edit,Write"]` に変えて、個別に add して
     commit する（この変更はまだしていません）。

2. 見張り係の起動：
   ```
   osascript -e 'tell application "Terminal" to do script "python3 ~/r2r2r-scripts/inbox/inbox_runner.py"'
   ```
   - Mac が「ターミナルを操作する許可」を求めたら許可してください。

結果を教えてもらえれば、この報告を書き足します。
````

### 1回目の疎通確認は20秒で戻り、夜の判定の食い違いも見つけた

見張り係を起動すると、置いておいた疎通確認の依頼を同じ秒に拾い、20秒後に「完了」と書きました。

```
2026-10-27T08:08:13 見張り係を起動
2026-10-27T08:08:13 開始 20261027_01_疎通確認.md
2026-10-27T08:08:33 完了 20261027_01_疎通確認.md → 結果/20261027_01_疎通確認_20261027_0808.md
```

依頼は、ファイルを変えずに4つを調べるだけのものです。Unity の版、画面なしの実行ファイルの作った日時、一番新しい夜の判定の件数、止められた操作があったか。止められた操作は0件で、auto はそのまま使えました。

報告の3つ目で、Claude Code は夜の判定（`judge.md`）の食い違いを書いてきました。「試算を見直す候補」は件数の表では0回なのに、本文の欄には2つの項目が載っています。判定のスクリプトは Claude が作ったもので、表と本文で数え方がずれている不具合です。

#### 20261027_01_疎通確認.md（依頼）

```markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 20
---
# 疎通確認（依頼箱の1本目）

依頼箱の自動の受け渡しが動くかを確かめるための依頼です。ファイルは何も変えないでください。

調べて報告すること:
1. Unity の版（ProjectSettings/ProjectVersion.txt）
2. 画面なしの実行ファイル（Builds/headless/）の build_info.json の要点（作った日時・changeset）。なければ「なし」
3. 一番新しい夜の判定（`/Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/` の一番新しい日付の judge.md）の振り分けの件数（正常・回の失敗・Unity の作り・試算を見直す候補）
4. この実行で止められた操作があったか（分かる範囲で）

報告は最後の返答だけでよいです（claude_code_reports には書かなくてよい）。
```

#### 20261027_01_疎通確認_20261027_0808.md（報告）

```markdown
# 結果：20261027_01_疎通確認

- 状態：完了
- 始めた時刻：2026-10-27 08:08
- 終わった時刻：2026-10-27 08:08（0分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume 6497c089-2d50-432f-8728-718207e83406`

## Claude Code の報告

やったこと
- 依頼にあった3つの場所を読みました。ファイルは変えていません。

結果
1. Unity の版は 2022.3.62f3（96770f904ca7）でした。
2. 画面なしの実行ファイル（Builds/headless/）の build_info.json の要点は次のとおりです。
   - 作った日時：2026-10-27 08:00:56
   - changeset：ServiceSim/cs:24（作りの番号は cs24_20261027_0800）
   - 作ったときに未登録の変更はありませんでした。
   - 対象は macOS の ARM64 で、場面は ServiceSim_Family.unity、大きさは約 60MB です。
3. 一番新しい夜は 20261027 でした。基準は 20261025 の第二段階の18回で、振り分けたのは70回です。
   - 正常：30回
   - 回の失敗：1回（fam_p1・ロボット2台・すれ違い入・種0の回で、全員が帰っていない）
   - Unity の作り：30回（4つの印。fam_p1 と fam_p5 のロボット1台で「30秒以上の止まり」が多い、fam_p5 のロボット1台で「動けなくなった」が多い、fam_p5 のロボット2台で「瞬間移動」が多い）
   - 試算を見直す候補：0回
     - ただし本文の「Unity÷試算 が基準の範囲から外れた」の欄には、fam_p5 の料理の待ちで2つの項目が載っています（ロボット1台が 1.24倍、ロボット2台が 1.4倍）。表の数と本文の書き方が食い違っています。
   - 基準なし：9回（ロボット2台・すれ違い入。基準に同じ条件がないため）
4. この実行で止められた操作はありませんでした。3回の読み取りはすべて通りました。

変えたファイル
- なし

決めてほしいこと
- なし
```

### 自動の実行だけ、APIキーの従量課金で払われていた

報告と一緒に残した、Claude Code のエラー出力（`_stderr.txt`）に、1行だけ警告がありました。

```
⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another auth source is set and takes precedence over your claude.ai login · Unset it to load your organization's connectors
```

「APIキーなどの別の認証が設定されているので、claude.ai のログインより優先している」という意味です。Claude Code の説明書には、`ANTHROPIC_API_KEY` が設定されていると、画面なしの実行（`-p`）では必ずその APIキーを使い、ふだんの画面では最初に1回だけ使うかを聞く、とあります。見張り係を起動したターミナルの窓には APIキーが設定されていて、見張り係から起動した Claude Code もそれを受け継いでいました。

ふだん手で使っている Claude Code の `/status` を開くと、ログインの方法は「Claude Max account」（定額のプラン）でした。手で使う分は定額、見張り係から起動した分だけ従量、という状態です。1回目の疎通確認は、Claude Code の見積もりで0.0997ドル（約16円）。軽い調べものでこの額なので、実装やビルドの依頼なら1本で数百円になりえます。

プランの使用量の画面も一緒に見ました。今週の枠はすでに73%使っていて、戻るのは10月29日の16時。追加の利用分が有効になっていて、枠を使い切るとそこから払われる設定です。同じ画面に、最近の使用量の99%が8時間以上続いた会話から、96%がやりとりが長く溜まった状態（15万トークン超）から、と出ていました。

定額の枠で回すか、APIの従量のままにするかは、次の4点を並べて決めました。

| 定額の枠で回すときの注意 | 中身 |
|---|---|
| 枠を食う | ふだんの Claude や Claude Code と同じ枠から減る。夜に重い依頼を回すと、翌日に使える分が減る |
| 枠を使い切ると止まる | その依頼は「失敗」に回る。追加の利用分が有効なので、使い切ったあとはそこから払われる |
| ログインが要る | Claude Code に claude.ai のアカウントでログインしていないと、APIキーを外した時点で動かない |
| コネクタが読み込まれる | APIキーを外すと、claude.ai でつないだ Notion・Google ドライブ・カレンダーなどが Claude Code で使える状態になる。無人の作業がそこに書き込める |

決めたのは、定額の枠で回すことでした。そのうえで、4つ目のコネクタは依頼箱の作業では使えないように止めます。

### 見張り係が Claude Code を起動するときだけ、APIキーを外した

ターミナルの窓の APIキーは、ほかの作業で使っているので消しません。見張り係が Claude Code を起動するときに渡す環境から、`ANTHROPIC_API_KEY` と `ANTHROPIC_AUTH_TOKEN` だけを抜きます。あわせて、claude.ai のコネクタを読み込まない設定を2か所に入れました（必ず断る操作の一覧に `disableClaudeAiConnectors`、渡す環境に `ENABLE_CLAUDEAI_MCP_SERVERS=false`）。エラー出力にもう一度同じ警告が出たら、報告の頭に★を付けて知らせます。

入れ替えは、見張り係を Ctrl+C で止めて、入れ替えのスクリプト（`update.sh`）を1行打つだけにしました。最新のファイルを iCloud からコピーし、そのファイルだけを commit し、設定を確かめてから起動し直します。

```
commit: 6aaacf4 依頼箱の見張り係を更新
依頼箱: /Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/依頼箱 あり
自動で回してよい場所: /Users/shimazueiichi/Robotics_UX/ServiceSim あり
claude: /Users/shimazueiichi/.npm-global/bin/claude
claude の版: 2.1.311 (Claude Code)
承認の扱い: auto ／ 拒否の決まり: /Users/shimazueiichi/r2r2r-scripts/inbox/settings.json
外す環境変数: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN ／ うち今この窓で設定されているもの: ANTHROPIC_API_KEY
2026-10-27T08:23:31 見張り係を起動
2026-10-27T08:23:31 開始 20261027_02_疎通確認_利用枠.md
2026-10-27T08:24:02 完了 20261027_02_疎通確認_利用枠.md → 結果/20261027_02_疎通確認_利用枠_20261027_0823.md
```

窓には APIキーが設定されたままで、2回目の疎通確認はそれでも APIキーなしで動いています。

- ねらい：依頼箱の作業を、APIの従量課金ではなく定額のプランの枠で動かす。無人の作業から claude.ai のコネクタを外す
- 変わったこと：1回目はエラー出力に APIキーの警告が出て、0.0997ドル（約16円）が従量で払われたとみられる。2回目はエラー出力が空で（`_stderr.txt` が作られなかった）、Claude Code が自分で調べた環境変数は `ANTHROPIC_API_KEY`・`ANTHROPIC_AUTH_TOKEN` ともに「なし」、claude.ai のコネクタの道具は0個
- 見込める効果：依頼箱の作業で、従量課金のお金が出なくなる。2回目の json にも見積もりの額（0.136ドル）は出るが、これは払い方に関係なく Claude Code が出す見積もりで、定額の枠の中で数えられる分。枠の減り方は、プランの使用量の表示で見ていく

報告の「止められた操作」の1件は、環境変数があるかを調べるコマンドの書き方（`${(P)v}` という、変数の名前から中身を引く書き方）が、判定の仕組みに断られたものです。Claude Code は `printenv` で調べ直していて、結果には影響していません。

#### 20261027_02_疎通確認_利用枠.md（依頼）

```markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 10
---
# 疎通確認の2回目（プランの利用枠で動いているか）

見張り係が APIキーを外して Claude Code を起動するように直しました。その確かめです。ファイルは何も変えないでください。

報告すること:
1. いま使える MCP のサーバーと道具の名前の一覧（claude.ai のコネクタ、たとえば Notion・Google ドライブ・カレンダーなどが含まれていないか）
2. 環境変数 ANTHROPIC_API_KEY と ANTHROPIC_AUTH_TOKEN が設定されているか（「あり／なし」だけ。値は書かない）

報告は最後の返答だけでよいです。
```

#### 20261027_02_疎通確認_利用枠_20261027_0823.md（報告）

```markdown
# 結果：20261027_02_疎通確認_利用枠

- 状態：完了
- 始めた時刻：2026-10-27 08:23
- 終わった時刻：2026-10-27 08:24（1分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume 9b566183-03a6-4adf-a005-b88c551f4659`
- 止められた操作：1件（いちばん下に一覧）

## Claude Code の報告

**やったこと**
- このセッションで使える道具の一覧を確認し、MCP のサーバーと道具を書き出しました。
- `printenv` で 2 つの環境変数が設定されているかだけを確認しました。値は出していません。
- ファイルは何も変えていません。

**結果**

1. **MCP のサーバーと道具**
   - 使える MCP の道具（`mcp__` で始まる名前のもの）は 0 個です。
   - 設定されている MCP サーバーは freee 関係の 2 つだけで、どちらも使えない状態です。
     - `plugin:freee-mcp:freee`：認証が済んでいないため使えません。
     - `plugin:freee-mcp:freee-mcp`：接続に失敗しました（「Connection closed」）。
   - claude.ai のコネクタ（Notion・Google ドライブ・カレンダーなど）は 1 つも含まれていません。
   - なお、「google-workspace」「docs」といった名前のスキルは一覧にありますが、これは MCP サーバーではありません。コネクタの道具も読み込まれていません。

2. **環境変数**
   - `ANTHROPIC_API_KEY`：なし
   - `ANTHROPIC_AUTH_TOKEN`：なし

   APIキーは外れていて、プランの利用枠で動いている状態とみられます。

**変えたファイル**
なし

**決めてほしいこと**
なし

（参考：freee の道具を使うには、対話できる画面で `/mcp` から認証するか、接続をやり直す必要があります。今回の依頼の範囲外なので、手は付けていません。）

## 止められた操作

- {"tool_name": "Bash", "tool_use_id": "toolu_01Y12aBuoWQNYcWtSaPPxv4w", "tool_input": {"command": "for v in ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN; do if [ -n \"${(P)v}\" ]; then echo \"$v: あり\"; else echo \"$v: なし\"; fi; done", "description": "Check whether two env vars are set (no values printed)"}}
```

### Mac にログインしたら、ターミナルの窓で見張り係が立ち上がるようにした

> もしMACの再起動とかでリセットかかった時のコマンドは？

見張り係は、ターミナルの窓を閉じたり Mac を再起動したりすると止まります。最初は、再起動のたびに起動のコマンドを1行打つ形でした。これを、Mac にログインしたときに自動で立ち上がる形にしました。

Mac には、ログインしたときに決めたプログラムを動かす仕組み（LaunchAgent）があります。ここに見張り係の Python を直接登録するのではなく、「ターミナルで `start_inbox.command` を開く」を登録しました。依頼箱は iCloud の「デスクトップ」の中にあり、裏で動くプログラムがここを読むには、Mac の許可が別に要る見込みです。ターミナルの窓で動かせば、今と同じ許可のまま動きます（裏で直接動かす形は試していません）。窓が開くので、見張り係が動いていることも目で分かります。

再起動したときに作業の途中だった依頼は、次に見張り係が起動したとき「失敗」へ回し、Mac に通知を出します。途中まで変わったファイルがあるかもしれないので、自動ではやり直しません。見張り係は2つ目を起動しないので、手でコマンドを重ねて打っても大丈夫です。

入れ替えのスクリプトがこの登録もするようにして、もう一度入れ替えました。

```
2026-10-27T08:32:28 見張り係を止めました
2026-10-27T08:32:36 見張り係を起動
```

本当にログインで立ち上がるかは、次に Mac を再起動したときに確かめます。立ち上がらなければ、`python3 ~/r2r2r-scripts/inbox/inbox_runner.py` で手で起動できます。

### 依頼箱の記録.log（10月27日の全部）

```
2026-10-27T08:08:13 見張り係を起動
2026-10-27T08:08:13 開始 20261027_01_疎通確認.md
2026-10-27T08:08:33 完了 20261027_01_疎通確認.md → 結果/20261027_01_疎通確認_20261027_0808.md
2026-10-27T08:22:03 「止める」があるので一時停止中
2026-10-27T08:23:24 見張り係を止めました
2026-10-27T08:23:31 見張り係を起動
2026-10-27T08:23:31 開始 20261027_02_疎通確認_利用枠.md
2026-10-27T08:24:02 完了 20261027_02_疎通確認_利用枠.md → 結果/20261027_02_疎通確認_利用枠_20261027_0823.md
2026-10-27T08:32:28 見張り係を止めました
2026-10-27T08:32:36 見張り係を起動
```

### inbox_runner.py（見張り係の全文・最後の版）

```python
#!/usr/bin/env python3
"""依頼箱の見張り係（inbox_runner）

依頼箱/新規/ に置かれた依頼（.md）を1件ずつ Claude Code（claude -p）に渡し、
Claude Code の報告を 依頼箱/結果/ に書き戻す。

使い方:
  python3 inbox_runner.py           見張りを続ける（ターミナルの窓を開けたままにする）
  python3 inbox_runner.py --once    今ある依頼を処理したら終わる
  python3 inbox_runner.py --check   設定・claude の場所と版を確かめるだけ

止め方:
  ターミナルで Ctrl+C。
  一時停止は 依頼箱/ に「止める」という名前のファイルを置く（消すと再開）。
"""
import argparse
import datetime as dt
import fcntl
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent
CONFIG = HERE / "config.json"
SETTINGS = HERE / "settings.json"
FOLDERS = ["新規", "確認待ち", "実行中", "済み", "失敗", "結果"]

PREAMBLE = """これは「依頼箱」から自動で渡された依頼です。人はこの画面を見ていません。質問はできません。

守ること:
- 依頼の範囲だけを行う。範囲の外は変えない。
- エイイチが決めること（方針・数値の決め方・公開・実機を動かすこと）に当たったら、そこで手を止め、最後の報告の「決めてほしいこと」に書く。推測で決めて進めない。
- プロジェクトの決まり（MASTER・CLAUDE.md）に従う。git は個別にファイルを指定して add する。git clean は使わない。
- 依頼文で報告ファイルの置き場所が指定されていれば、そこにも書く。
- 最後の返答は日本語の報告にする。英語の文を混ぜない。形は次のとおり:
  やったこと／結果（数字があれば数字）／変えたファイル／決めてほしいこと（なければ「なし」）

依頼ファイル: {name}
----- ここから依頼 -----
"""


def now():
    return dt.datetime.now()


def stamp():
    return now().strftime("%Y%m%d_%H%M")


def load_config():
    with open(CONFIG, encoding="utf-8") as f:
        cfg = json.load(f)
    cfg["inbox"] = Path(os.path.expanduser(cfg["inbox"]))
    cfg["allowed_dirs"] = [Path(os.path.expanduser(p)).resolve() for p in cfg["allowed_dirs"]]
    return cfg


def log(cfg, msg):
    line = f"{now().isoformat(timespec='seconds')} {msg}"
    print(line, flush=True)
    try:
        with open(cfg["inbox"] / "記録.log", "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except OSError as e:
        print(f"記録.log に書けません: {e}", flush=True)


def notify(title, msg):
    """Mac の通知を出す（失敗しても止めない）"""
    if sys.platform != "darwin":
        return
    msg = msg.replace("\n", " ")
    script = (f"display notification {json.dumps(msg, ensure_ascii=False)} "
              f"with title {json.dumps(title, ensure_ascii=False)}")
    try:
        subprocess.run(["osascript", "-e", script], capture_output=True, timeout=10)
    except Exception:
        pass


HEADER_RE = re.compile(r"\A---[ \t]*\n(.*?)\n---[ \t]*\n", re.S)


def parse_request(text):
    """頭の --- で囲んだ部分を読む。例: 作業フォルダ: /Users/...  上限時間: 120"""
    text = text.lstrip("﻿").replace("\r\n", "\n")
    meta, body = {}, text
    m = HEADER_RE.match(text)
    if m:
        for line in m.group(1).splitlines():
            parts = re.split(r"[:：]", line, maxsplit=1)
            if len(parts) == 2:
                meta[parts[0].strip()] = parts[1].strip()
        body = text[m.end():]
    return meta, body


def is_allowed(cfg, workdir):
    for base in cfg["allowed_dirs"]:
        if workdir == base or base in workdir.parents:
            return True
    return False


def pending(cfg):
    d = cfg["inbox"] / "新規"
    items = []
    for p in sorted(d.iterdir()):
        n = p.name
        if n.startswith(".") and n.endswith(".icloud"):
            # iCloud に退避されたファイルは取り寄せてから次の回で拾う
            real = d / n[1:-len(".icloud")]
            subprocess.run(["brctl", "download", str(real)], capture_output=True)
            continue
        if n.startswith(".") or not n.endswith(".md"):
            continue
        if time.time() - p.stat().st_mtime < cfg.get("settle_seconds", 15):
            continue  # 書き込み中かもしれない
        items.append(p)
    return items


def move(src, folder):
    dst = src.parent.parent / folder / src.name
    if dst.exists():
        dst = dst.with_name(f"{dst.stem}_{stamp()}{dst.suffix}")
    shutil.move(str(src), str(dst))
    return dst


def child_env(cfg):
    """Claude Code に渡す環境。APIキーを外してプランの利用枠で動かす（config.json の drop_env / set_env）"""
    env = dict(os.environ)
    for k in cfg.get("drop_env", []):
        env.pop(k, None)
    env.update({k: str(v) for k, v in cfg.get("set_env", {}).items()})
    return env


def run_claude(cfg, prompt, workdir, limit_min):
    cmd = [cfg.get("claude", "claude"), "-p", prompt,
           "--output-format", "json",
           "--permission-mode", cfg["permission_mode"],
           "--permission-prompts", "none",
           "--settings", str(SETTINGS)] + list(cfg.get("extra_args", []))
    # 出力はファイルで受ける（Claude Code が残した子プロセスが管をふさいで待ち続けるのを防ぐ）
    with tempfile.TemporaryFile("w+", encoding="utf-8") as fo, \
            tempfile.TemporaryFile("w+", encoding="utf-8") as fe:
        p = subprocess.Popen(cmd, cwd=str(workdir), stdout=fo, stderr=fe,
                             start_new_session=True, env=child_env(cfg))
        timed_out = False
        try:
            p.wait(timeout=limit_min * 60)
        except subprocess.TimeoutExpired:
            timed_out = True
            # 先に SIGINT（その回の作業を終わらせる）→ だめなら SIGTERM → 最後に SIGKILL
            for sig, wait in ((signal.SIGINT, 120), (signal.SIGTERM, 60), (signal.SIGKILL, 30)):
                try:
                    os.killpg(p.pid, sig)
                except ProcessLookupError:
                    pass
                try:
                    p.wait(timeout=wait)
                    break
                except subprocess.TimeoutExpired:
                    continue
        fo.seek(0)
        fe.seek(0)
        return p.returncode, fo.read(), fe.read(), timed_out


def write_report(cfg, req, meta, workdir, started, rc, out, err, timed_out, note=""):
    res_dir = cfg["inbox"] / "結果"
    base = f"{req.stem}_{started.strftime('%Y%m%d_%H%M')}"
    data = None
    try:
        data = json.loads(out) if out.strip() else None
    except json.JSONDecodeError:
        data = None
    if out.strip():
        (res_dir / f"{base}.json").write_text(out, encoding="utf-8")
    if err.strip():
        (res_dir / f"{base}_stderr.txt").write_text(err, encoding="utf-8")

    is_error = bool(data.get("is_error")) if isinstance(data, dict) else True
    if timed_out:
        state = "時間切れ（途中で止めました）"
    elif note:
        state = "失敗"
    elif rc == 0 and not is_error:
        state = "完了"
    else:
        state = "失敗"

    ended = now()
    mins = (ended - started).total_seconds() / 60
    lines = [f"# 結果：{req.stem}", "",
             f"- 状態：{state}",
             f"- 始めた時刻：{started.strftime('%Y-%m-%d %H:%M')}",
             f"- 終わった時刻：{ended.strftime('%Y-%m-%d %H:%M')}（{mins:.0f}分）",
             f"- 作業フォルダ：{workdir}"]
    if isinstance(data, dict) and data.get("session_id"):
        lines.append(f"- この続きを Claude Code で話すとき：作業フォルダで `claude --resume {data['session_id']}`")
    denials = data.get("permission_denials") if isinstance(data, dict) else None
    if denials:
        lines.append(f"- 止められた操作：{len(denials)}件（いちばん下に一覧）")
    if "ANTHROPIC_API_KEY" in err or "auth source is set" in err:
        lines.append("- ★ APIキーで動いた可能性があります（プランの利用枠ではなく従量課金）。_stderr.txt を見てください")
    lines += ["", "## Claude Code の報告", ""]
    if note:
        lines.append(note)
    elif isinstance(data, dict) and data.get("result"):
        lines.append(str(data["result"]))
    else:
        lines.append("（報告がありません。終了コード " + str(rc) + "。_stderr.txt を見てください。ログインが切れているときは、ターミナルで claude を開いて /login）")
    if denials:
        lines += ["", "## 止められた操作", ""]
        for d in denials:
            lines.append("- " + json.dumps(d, ensure_ascii=False)[:500])
    path = res_dir / f"{base}.md"
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")
    return state, path


def handle(cfg, req):
    started = now()
    req = move(req, "実行中")
    log(cfg, f"開始 {req.name}")
    meta, body = parse_request(req.read_text(encoding="utf-8"))

    wd_text = meta.get("作業フォルダ", "")
    workdir = Path(os.path.expanduser(wd_text)).resolve() if wd_text else None
    note = ""
    if not workdir:
        note = "依頼の頭に「作業フォルダ:」がありません。実行していません。"
    elif not workdir.is_dir():
        note = f"作業フォルダが見つかりません：{workdir}。実行していません。"
    elif not is_allowed(cfg, workdir):
        note = (f"作業フォルダ {workdir} は自動で回してよい場所（config.json の allowed_dirs）に入っていません。"
                "実行していません。手で頼むか、allowed_dirs に足すかを決めてください。")

    try:
        limit = int(re.sub(r"[^0-9]", "", meta.get("上限時間", "")) or cfg["default_limit_minutes"])
    except ValueError:
        limit = cfg["default_limit_minutes"]

    if note:
        rc, out, err, timed_out = -1, "", "", False
    else:
        prompt = PREAMBLE.format(name=req.name) + body
        try:
            rc, out, err, timed_out = run_claude(cfg, prompt, workdir, limit)
        except FileNotFoundError:
            rc, out, err, timed_out = -1, "", "", False
            note = "claude が見つかりません。config.json の claude に絶対パスを書いてください。"

    state, report = write_report(cfg, req, meta, workdir, started, rc, out, err, timed_out, note)
    move(req, "済み" if state == "完了" else "失敗")
    log(cfg, f"{state} {req.name} → 結果/{report.name}")
    notify("依頼箱", f"{state}：{req.stem}")


def check(cfg):
    ok = True
    print("依頼箱:", cfg["inbox"], "あり" if cfg["inbox"].is_dir() else "★ ない")
    ok &= cfg["inbox"].is_dir()
    for d in cfg["allowed_dirs"]:
        print("自動で回してよい場所:", d, "あり" if d.is_dir() else "★ ない")
    exe = shutil.which(cfg.get("claude", "claude"))
    print("claude:", exe or "★ 見つからない")
    if exe:
        v = subprocess.run([exe, "--version"], capture_output=True, text=True)
        print("claude の版:", (v.stdout or v.stderr).strip())
    else:
        ok = False
    print("承認の扱い:", cfg["permission_mode"], "／ 拒否の決まり:", SETTINGS)
    print("外す環境変数:", ", ".join(cfg.get("drop_env", [])) or "なし",
          "／ うち今この窓で設定されているもの:",
          ", ".join(k for k in cfg.get("drop_env", []) if k in os.environ) or "なし")
    return ok


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--once", action="store_true")
    ap.add_argument("--check", action="store_true")
    a = ap.parse_args()
    cfg = load_config()
    if a.check:
        sys.exit(0 if check(cfg) else 1)

    for f in FOLDERS:
        (cfg["inbox"] / f).mkdir(parents=True, exist_ok=True)

    lock = open(HERE / ".runner.lock", "w")
    try:
        fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except BlockingIOError:
        print("見張り係はもう動いています（2つ目は起動しません）")
        sys.exit(1)

    if sys.platform == "darwin" and not a.once:
        # 見張っている間は Mac をスリープさせない（この係が終わると解除）
        subprocess.Popen(["caffeinate", "-i", "-w", str(os.getpid())])

    log(cfg, "見張り係を起動" + ("（1回だけ）" if a.once else ""))

    # 前回、作業の途中で止まった依頼（Mac の再起動など）は「失敗」へ回す。
    # 途中まで変わったファイルがあるかもしれないので、自動ではやり直さない。
    for stale in sorted((cfg["inbox"] / "実行中").glob("*.md")):
        write_report(cfg, stale, {}, "不明", now(), -1, "", "", False,
                     note="見張り係が止まった（Mac の再起動など）ため、この依頼は途中で終わっています。"
                          "途中まで変わったファイルがあるかもしれません。やり直すかどうかは、確かめてから決めます。")
        move(stale, "失敗")
        log(cfg, f"中断されていた依頼を失敗へ {stale.name}")
        notify("依頼箱", f"中断されていた依頼があります：{stale.stem}")
    paused_logged = False
    try:
        while True:
            if (cfg["inbox"] / "止める").exists():
                if not paused_logged:
                    log(cfg, "「止める」があるので一時停止中")
                    paused_logged = True
            else:
                paused_logged = False
                for req in pending(cfg):
                    if (cfg["inbox"] / "止める").exists():
                        break
                    try:
                        handle(cfg, req)
                    except Exception as e:  # 1件の失敗で見張りを止めない
                        log(cfg, f"★ {req.name} の処理中に失敗: {e!r}")
            if a.once:
                break
            time.sleep(cfg.get("poll_seconds", 30))
    except KeyboardInterrupt:
        log(cfg, "見張り係を止めました")


if __name__ == "__main__":
    main()
```

### config.json（見張り係の設定）

```json
{
  "inbox": "~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/依頼箱",
  "allowed_dirs": [
    "/Users/shimazueiichi/Robotics_UX/ServiceSim"
  ],
  "claude": "/Users/shimazueiichi/.npm-global/bin/claude",
  "permission_mode": "auto",
  "default_limit_minutes": 180,
  "poll_seconds": 30,
  "settle_seconds": 15,
  "extra_args": [],
  "drop_env": [
    "ANTHROPIC_API_KEY",
    "ANTHROPIC_AUTH_TOKEN"
  ],
  "set_env": {
    "ENABLE_CLAUDEAI_MCP_SERVERS": "false"
  }
}
```

### settings.json（Claude Code に必ず断らせる操作）

```json
{
  "disableClaudeAiConnectors": true,
  "permissions": {
    "deny": [
      "Bash(git clean)",
      "Bash(git clean *)",
      "Bash(git add .)",
      "Bash(git add -A)",
      "Bash(git add -A *)",
      "Bash(git add --all)",
      "Bash(git add --all *)",
      "Bash(git push --force *)",
      "Bash(git push -f *)",
      "Bash(git reset --hard *)",
      "Bash(rm -rf *)",
      "Bash(sudo *)"
    ]
  }
}
```

### update.sh（入れ替えと起動し直しのスクリプト）

```bash
#!/bin/bash
# 依頼箱の見張り係を、iCloud の _仕組み/ の最新に入れ替えて commit し、起動し直す。
# 使い方：見張り係の窓で Ctrl+C で止めてから
#   bash ~/r2r2r-scripts/inbox/update.sh
set -e
INBOX="$HOME/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/依頼箱"
SRC="$INBOX/_仕組み"
DST="$HOME/r2r2r-scripts/inbox"
FILES="inbox_runner.py config.json settings.json update.sh start_inbox.command com.pixelstomotors.inbox.plist"

for f in $FILES; do cp "$SRC/$f" "$DST/$f"; done
chmod +x "$DST/start_inbox.command"
xattr -d com.apple.quarantine "$DST/start_inbox.command" 2>/dev/null || true

# Mac にログインしたら見張り係が自動で起動するように登録（次のログインから効く）
mkdir -p "$HOME/Library/LaunchAgents"
plutil -lint "$DST/com.pixelstomotors.inbox.plist" >/dev/null
cp "$DST/com.pixelstomotors.inbox.plist" "$HOME/Library/LaunchAgents/"
echo "自動起動：登録済み（$HOME/Library/LaunchAgents/com.pixelstomotors.inbox.plist）"

cd "$HOME/r2r2r-scripts"
PATHS=""
for f in $FILES; do PATHS="$PATHS inbox/$f"; done
git add $PATHS
# この6つだけを commit する（ほかに add 済みのものがあっても混ぜない）
if ! git diff --cached --quiet -- $PATHS; then
  git commit -q -m "依頼箱の見張り係を更新" -- $PATHS
fi
echo "commit: $(git log -1 --oneline -- inbox)"

python3 "$DST/inbox_runner.py" --check
rm -f "$INBOX/止める"
exec python3 "$DST/inbox_runner.py"
```

### start_inbox.command（ログインしたときにターミナルで開くファイル）

```bash
#!/bin/bash
# 依頼箱の見張り係を起動する（Mac にログインしたとき、ターミナルの窓で自動で開く）
cd "$HOME"
exec python3 "$HOME/r2r2r-scripts/inbox/inbox_runner.py"
```

### com.pixelstomotors.inbox.plist（ログインしたときに動かす登録）

```xml
<?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.pixelstomotors.inbox</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/bin/open</string>
		<string>-a</string>
		<string>Terminal</string>
		<string>/Users/shimazueiichi/r2r2r-scripts/inbox/start_inbox.command</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
</dict>
</plist>
```

### README.md（依頼箱の説明・依頼箱のフォルダに置いたもの）

````markdown
# 依頼箱

Claude が書いた Claude Code あての依頼を、Mac の見張り係が自動で Claude Code に渡し、報告を書き戻す場所。

## フォルダ

| フォルダ | 中身 |
|---|---|
| 新規 | Claude が依頼を置く。見張り係が1件ずつ自動で実行する |
| 確認待ち | エイイチの確認が要る依頼。読んで良ければ「新規」へ移すと実行される |
| 実行中 | いま Claude Code が作業している依頼 |
| 済み／失敗 | 終わった依頼 |
| 結果 | Claude Code の報告（.md）と元の出力（.json） |
| _仕組み | 見張り係の元のファイル（据え付け先は Mac の `~/r2r2r-scripts/inbox/`） |

`記録.log` に、いつ何を始めて終えたかが残る。終わると Mac に通知が出る。

## 決まり

- 自動で回してよいのは `config.json` の `allowed_dirs` にある作業フォルダだけ（今は ServiceSim だけ）。ほかの場所の依頼は実行せずに「失敗」へ回す
- 実機を動かす依頼は自動にしない。関節制御・姿勢系の変更は、実装とレビューまでを自動にし、実機に流す前は「確認待ち」で止める
- `git clean`・`git add .`・強制 push・`rm -rf` などは `settings.json` で拒否してある
- Claude Code が決めるべきでないことに当たったら、手を止めて報告の「決めてほしいこと」に書く

## 起動・停止

- 起動：Mac にログインすると、ターミナルの窓が自動で開いて起動する（登録は `~/Library/LaunchAgents/com.pixelstomotors.inbox.plist`）。窓は開けたまま。見張っている間は Mac がスリープしない
- 手で起動するとき：ターミナルで `python3 ~/r2r2r-scripts/inbox/inbox_runner.py`（2つ目は起動しないので、重ねて打っても大丈夫）
- 作業の途中で Mac が止まった依頼は、次に起動したときに「失敗」へ回る（自動ではやり直さない）
- 見張り係を新しくするとき：`bash ~/r2r2r-scripts/inbox/update.sh`
- 停止：その窓で Ctrl+C
- 一時停止：このフォルダに「止める」という名前のファイルを置く（消すと再開）

## 依頼の書き方（Claude が書く）

```
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 120
---
（依頼の本文）
```

上限時間は分。書かなければ180分。時間を過ぎたら止めて「時間切れ」として報告する。
````

### 据え付け依頼.md（Claude Code に1回だけ手で渡した依頼）

```markdown
# 据え付け依頼：依頼箱の見張り係（Claude Code あて・この1回だけ手で渡す）

依頼箱は、Claude が書いた依頼を Claude Code に自動で渡し、報告を書き戻す仕組みです。
この依頼では、見張り係を Mac に据え付けて、動き始めるところまでを行ってください。

元のファイルの場所（iCloud）:
`~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/依頼箱/_仕組み/`

## 手順

1. 元のファイル3つ（`inbox_runner.py`・`config.json`・`settings.json`）を `~/r2r2r-scripts/inbox/` にコピーする。
   - `~/r2r2r-scripts` の git に、3つを個別に指定して add し、commit する（`git add .` と `git clean` は使わない）。
   - `.runner.lock` は commit しない（`~/r2r2r-scripts/inbox/.gitignore` に1行足して、それも個別に add する）。
2. `claude --version` を確かめる。2.1.259 より古ければ `claude update` で上げる（`--permission-prompts` を使うため）。
3. `python3 ~/r2r2r-scripts/inbox/inbox_runner.py --check` を実行し、全部「あり」になるか確かめる。
   - claude が見つからないと出たら、`which claude` の絶対パスを `config.json` の `"claude"` に書く。
4. 承認の扱い「auto」が使えるかを確かめる。`/Users/shimazueiichi/Robotics_UX/ServiceSim` で次を実行する。
   `claude -p "OKとだけ返して" --output-format json --permission-mode auto --permission-prompts none --settings ~/r2r2r-scripts/inbox/settings.json`
   - 使えない（auto が無効・選べないという内容の誤り）なら、`config.json` を次のように変えて、もう一度確かめる。
     `"permission_mode": "acceptEdits"`、`"extra_args": ["--allowedTools", "Bash,Read,Edit,Write"]`
   - 変えたら、その変更も個別に add して commit する。
5. 見張り係を、新しいターミナルの窓で起動する。
   `osascript -e 'tell application "Terminal" to do script "python3 ~/r2r2r-scripts/inbox/inbox_runner.py"'`
   - Mac が「ターミナルを操作する許可」を求めたら、エイイチに許可してもらう。
   - 起動すると、依頼箱/新規/ にある「疎通確認」の依頼を自動で拾って実行する。

## 報告

`依頼箱/結果/据え付け_20261027.md` に、次を日本語で書いてください。
- 各手順の結果（claude の版・`--check` の出力・auto が使えたか・config.json を変えたか）
- commit の番号
- 見張り係が起動したか

Assets や ServiceSim のプロジェクトは変えないでください。
```

### この日の公開

- 記事MD・サムネイル・図解：iCloud の `PixelstoMotors/デジタルツイン学習R2R2R/記事/` と、GitHub `PixelstoMotors/r2r2r-content` の `articles/`
- SNS の txt・json：同じ作業フォルダと、r2r2r-content の `sns/`
- 公開：GitHub `PixelstoMotors/p2m-site`（https://pixelstomotors.com/record/claude-code-api-key-billing ）
- 見張り係のコード：Mac の `~/r2r2r-scripts/inbox/`（commit 636f374・6aaacf4 と、自動起動を足した commit）。元のファイルは依頼箱の `_仕組み/`
- 次に確かめること：Mac を再起動したときに見張り係が立ち上がるか。依頼箱に流す最初の本番の依頼（夜の判定の食い違いの直し）が、人の手を通らずに戻ってくるか
