Auto-Generating an Index of Your Claude Code Custom Agents from Their Frontmatter A developer created an auto-generated index for Claude Code custom agents stored in ~/.claude/agents/, after manually managing 27 agents became unsustainable. The Bash script with embedded Python parses frontmatter from agent .md files and produces a table with Name, Model, Description, and Tools columns, plus an optional JSON output for other scripts. This is a continuation of my "Claude Code environment" series. In the previous post, Automatically thinning conversation logs to prevent bloat https://zenn.dev/bokuwalily/articles/claude-logs-autodiet , I introduced the basic pattern for scheduled launchd jobs. This time I'm using that same mechanism to automatically maintain a list of the custom agents in ~/.claude/agents/ . Dropping a single .md file into ~/.claude/agents/ adds a custom agent, but before long you lose track of how many you have, what model each one uses, and which tools each is allowed to touch. That's exactly what happened to me with the 27 agents I now have. I tried writing an INDEX.md by hand to manage them, and of course within a few days it had drifted from reality. Manually updating INDEX.md every time you add a custom agent is not sustainable. name or description and never noticeI concluded there was no sustainable way to manage this other than "generate it automatically," so I wrote agents-index.sh . Here's how the top of my current ~/.claude/agents/INDEX.md looks. < -- 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" | Four columns: Name, Model, Description, and Tools. You can see at a glance how the models break down across opus / sonnet / haiku , and immediately check things like "did this agent have Bash allowed?" Running it with the --json option also writes the same content to .index.json , so other scripts like a cost-tracker can reuse it. ~/.claude/scripts/agents-index.sh is a Bash outer shell with an inline Python3 script embedded in it. bash /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 The reason for embedding Python as a heredoc is to hand string processing off to Python while avoiding shell PATH problems. Parsing YAML in pure Bash is dangerous, and assuming pip install pyyaml would only add more environment dependencies. Agent definitions only ever use single-level key-value pairs, so a regex plus line splitting is enough. python 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 grabs everything between the --- markers wholesale, then partitioning on : makes everything left of the first : the key. JSON array values like tools are kept as plain strings and truncated at 60 characters for display. An agent with an empty name or description is useless even if it shows up in the list. After parsing, the script checks the required fields and appends a warnings section at the end of INDEX.md. if not name: warnings.append f"{p.name}: missing 'name'" if not desc: warnings.append f"{p.name}: missing 'description'" The end of the generated output looks like this. ⚠️ Validation warnings - foo-agent.md: missing 'description' Note Files that are missing frontmatter entirely get routed to the warnings as "missing frontmatter" and don't appear in the table. I designed it to record a warning instead of failing with an error so that INDEX.md generation itself still succeeds while keeping it clear which file is broken. The StartCalendarInterval in ~/Library/LaunchAgents/com.shun.agents-index.plist looks like this.