「Claude Code環境」シリーズの続きです。前作 会話ログを自動で間引いて肥大化を防ぐ で launchd 定期ジョブの基本パターンを紹介しました。今回は同じ仕組みを使って ~/.claude/agents/ のカスタムエージェント一覧を自動で維持する 話です。
~/.claude/agents/ に .md を1ファイル置くとカスタムエージェントが増やせますが、気づくと「何個あって、それぞれ model は何で、どの tool を許可しているか」が把握できなくなります。現在 27 個ある私の環境でそれが起きました。手で INDEX.md を書いて管理しようとしたところ、当然数日で実態と乖離します。
困りごと:一覧が腐る
カスタムエージェントを追加するたびに INDEX.md を手で更新するのは無理です。
- 追加したことを忘れて書き洩らす
- model を後から変えても INDEX.md に反映しない
-
nameやdescriptionを typo しても気づかない
「自動で生成する」以外に持続可能な管理方法がないと判断して agents-index.sh を作りました。
生成物:INDEX.md の実例
現時点の ~/.claude/agents/INDEX.md の冒頭はこうなっています。
<!-- AUTO-GENERATED by ~/.claude/scripts/agents-index.sh — DO NOT EDIT MANUALLY -->
# Agents Index (27 agents · 2026-07-28 02:02)
| Name | Model | Description | Tools |
|------|-------|-------------|-------|
| `architect` ([architect.md](./architect.md)) | opus | Software architecture specialist ... | ["Read", "Grep", "Glob"] |
| `build-error-resolver` ([build-error-resolver.md](./build-error-resolver.md)) | sonnet | Build and TypeScript error resolution specialist ... | ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] |
| `doc-updater` ([doc-updater.md](./doc-updater.md)) | haiku | Documentation and codemap specialist ... | ["Read", "Edit", "Bash", "Grep", "Glob"] |
Name・Model・Description・Tools の4列。model が opus / sonnet / haiku で色分けされていることが一目でわかり、「この agent は Bash を許可していたか」もすぐ確認できます。
--json オプション付きで実行すると同じ内容を .index.json にも書き出すので、cost-tracker など他のスクリプトから再利用できます。
スクリプトの設計
~/.claude/scripts/agents-index.sh は Bash の外殻に Python3 インラインスクリプトを埋め込む構成です。
#!/usr/bin/env bash
set -uo pipefail
AGENTS_DIR="$HOME/.claude/agents"
INDEX_MD="$AGENTS_DIR/INDEX.md"
INDEX_JSON="$AGENTS_DIR/.index.json"
LOGFILE="$HOME/.claude/logs/agents-index.log"
mkdir -p "$HOME/.claude/logs"
EMIT_JSON=0
[ "${1:-}" = "--json" ] && EMIT_JSON=1
python3 - "$AGENTS_DIR" "$INDEX_MD" "$INDEX_JSON" "$EMIT_JSON" "$LOGFILE" <<'PY'
# ... (Python本体)
PY
Python をヒアドキュメントで埋め込む理由は、シェルの PATH 問題を回避しながら文字列処理を Python に任せるためです。Bash 単体で YAML をパースするのは危険で、かといって pip install pyyaml を前提にすると環境依存が増えます。
PyYAML 非依存の frontmatter パーサ
エージェント定義は「1階層のキーバリュー」しか使わないため、正規表現と行分割で十分です。
def load_frontmatter(path):
text = path.read_text(encoding="utf-8", errors="replace")
m = re.match(r"^---\n(.*?)\n---\n", text, flags=re.DOTALL)
if not m:
return None, "missing frontmatter"
body = m.group(1)
out = {}
for line in body.split("\n"):
if not line.strip() or line.startswith("#"):
continue
if ":" not in line:
continue
k, _, v = line.partition(":")
k = k.strip()
v = v.strip()
# quoted string 剥がす
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
v = v[1:-1]
out[k] = v
return out, None
re.DOTALL で --- 間を丸ごと取り、: で partition して最初の : より左をキーにします。tools のような JSON 配列値は文字列のまま扱い、表示時に 60 文字で打ち切ります。
欠落フィールドの警告
name や description が空のエージェントは一覧に載っても役に立ちません。パース後に必須フィールドをチェックし、INDEX.md の末尾に警告セクションを追記します。
if not name:
warnings.append(f"{p.name}: missing 'name'")
if not desc:
warnings.append(f"{p.name}: missing 'description'")
生成結果の末尾はこうなります。
## ⚠️ Validation warnings
- foo-agent.md: missing 'description'
frontmatter がまるごと抜けているファイルは "missing frontmatter" として警告に回り、テーブルには載りません。エラーで止まらず警告として記録だけする設計にしたのは、INDEX.md の生成自体は成功させて「壊れているのはこのファイルだ」と分かる状態を維持するためです。
launchd 設定:日曜 6:30 に自動実行
~/Library/LaunchAgents/com.shun.agents-index.plist の StartCalendarInterval がこうなっています。
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>6</integer>
<key>Minute</key>
<integer>30</integer>
<key>Weekday</key>
<integer>0</integer>
</dict>
Weekday: 0 が日曜。週明けに仕事を始める前に常に最新の一覧が手元にある、という設計です。
プロセス優先度は Nice: 10 + LowPriorityIO: true + ProcessType: Background の三重で下げています。INDEX 生成はディスク I/O を伴いますが作業の優先度を下げることで Claude Code 本体の応答に影響させません。
ロードは launchctl load で一度だけ。
launchctl load ~/Library/LaunchAgents/com.shun.agents-index.plist
手動で今すぐ走らせたい場合は直接叩きます。
~/.claude/scripts/agents-index.sh --json
# → agents=27 warnings=0 → ~/.claude/agents/INDEX.md
踏んだ落とし穴
-
INDEX.md 自身がループ対象になる →
if p.name == "INDEX.md": continueで除外が必要。入れ忘れると INDEX が INDEX を読んで壊れる -
description の
|でテーブルが崩れる →.replace("|", "\\|")をパーサ後に必ず通す。実コードにも入れてあるが最初は忘れていた -
tools が JSON 配列文字列で来るため 60 字制限を超えやすい →
["Read", "Write", "Edit", "Bash", "Grep", "Glob"]で既に 50 字近い。MCP ツールが混ざると即 overflow するので打ち切り +...が必須 -
plist に StandardOutPath と ProgramArguments の
>>リダイレクトを両方書くと二重ログになる → 実 plist ではどちらも同じファイルを向いているため実害はないが、整理するなら片方に統一すべき -
mac がスリープ中だと StartCalendarInterval はスキップされる → launchd は目覚めてから実行するが、スリープのまま日曜を越えると月曜朝まで更新されない。週次更新で十分なので受け入れているが、厳密にしたいなら
pmset -a wake 1併用
まとめ
-
~/.claude/agents/が増えてくると手動の INDEX.md は必ず腐る → 生成を自動化する - PyYAML 非依存の1行パーサで frontmatter を読み、Markdown テーブルと
.index.jsonを生成 - 欠落フィールドは警告セクションとして INDEX.md 末尾に追記し、スクリプト自体は止めない
- launchd の
Weekday: 0 / Hour: 6 / Minute: 30で日曜 6:30 に自動実行。Nice: 10+LowPriorityIOで作業の邪魔をしない - 手動でも
agents-index.sh --json一発で即時更新できる
次回は、このエージェント一覧を使って「model ごとのコスト傾向を可視化する」仕組みの話を書く予定です。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 制作物・記事は bokuwalily.com にまとめています🖥️
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!