{"slug": "auto-generating-an-index-of-your-claude-code-custom-agents-from-their", "title": "Auto-Generating an Index of Your Claude Code Custom Agents from Their Frontmatter", "summary": "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.", "body_md": "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/**.\n\nDropping a single .md file into `~/.claude/agents/`\n\nadds 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.\n\nManually updating INDEX.md every time you add a custom agent is not sustainable.\n\n`name`\n\nor `description`\n\nand never noticeI concluded there was no sustainable way to manage this other than \"generate it automatically,\" so I wrote `agents-index.sh`\n\n.\n\nHere's how the top of my current `~/.claude/agents/INDEX.md`\n\nlooks.\n\n```\n<!-- AUTO-GENERATED by ~/.claude/scripts/agents-index.sh — DO NOT EDIT MANUALLY -->\n# Agents Index (27 agents · 2026-07-28 02:02)\n\n| Name | Model | Description | Tools |\n|------|-------|-------------|-------|\n| `architect` ([architect.md](./architect.md)) | opus | Software architecture specialist ... | [\"Read\", \"Grep\", \"Glob\"] |\n| `build-error-resolver` ([build-error-resolver.md](./build-error-resolver.md)) | sonnet | Build and TypeScript error resolution specialist ... | [\"Read\", \"Write\", \"Edit\", \"Bash\", \"Grep\", \"Glob\"] |\n| `doc-updater` ([doc-updater.md](./doc-updater.md)) | haiku | Documentation and codemap specialist ... | [\"Read\", \"Edit\", \"Bash\", \"Grep\", \"Glob\"] |\n```\n\nFour columns: Name, Model, Description, and Tools. You can see at a glance how the models break down across `opus`\n\n/ `sonnet`\n\n/ `haiku`\n\n, and immediately check things like \"did this agent have Bash allowed?\"\n\nRunning it with the `--json`\n\noption also writes the same content to `.index.json`\n\n, so other scripts like a cost-tracker can reuse it.\n\n`~/.claude/scripts/agents-index.sh`\n\nis a Bash outer shell with an inline Python3 script embedded in it.\n\n``` bash\n#!/usr/bin/env bash\nset -uo pipefail\n\nAGENTS_DIR=\"$HOME/.claude/agents\"\nINDEX_MD=\"$AGENTS_DIR/INDEX.md\"\nINDEX_JSON=\"$AGENTS_DIR/.index.json\"\nLOGFILE=\"$HOME/.claude/logs/agents-index.log\"\nmkdir -p \"$HOME/.claude/logs\"\n\nEMIT_JSON=0\n[ \"${1:-}\" = \"--json\" ] && EMIT_JSON=1\n\npython3 - \"$AGENTS_DIR\" \"$INDEX_MD\" \"$INDEX_JSON\" \"$EMIT_JSON\" \"$LOGFILE\" <<'PY'\n# ... (Python本体)\nPY\n```\n\nThe 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`\n\nwould only add more environment dependencies.\n\nAgent definitions only ever use single-level key-value pairs, so a regex plus line splitting is enough.\n\n``` python\ndef load_frontmatter(path):\n    text = path.read_text(encoding=\"utf-8\", errors=\"replace\")\n    m = re.match(r\"^---\\n(.*?)\\n---\\n\", text, flags=re.DOTALL)\n    if not m:\n        return None, \"missing frontmatter\"\n    body = m.group(1)\n    out = {}\n    for line in body.split(\"\\n\"):\n        if not line.strip() or line.startswith(\"#\"):\n            continue\n        if \":\" not in line:\n            continue\n        k, _, v = line.partition(\":\")\n        k = k.strip()\n        v = v.strip()\n        # quoted string 剥がす\n        if (v.startswith('\"') and v.endswith('\"')) or (v.startswith(\"'\") and v.endswith(\"'\")):\n            v = v[1:-1]\n        out[k] = v\n    return out, None\n```\n\n`re.DOTALL`\n\ngrabs everything between the `---`\n\nmarkers wholesale, then partitioning on `:`\n\nmakes everything left of the first `:`\n\nthe key. JSON array values like `tools`\n\nare kept as plain strings and truncated at 60 characters for display.\n\nAn agent with an empty `name`\n\nor `description`\n\nis 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.\n\n```\nif not name:\n    warnings.append(f\"{p.name}: missing 'name'\")\nif not desc:\n    warnings.append(f\"{p.name}: missing 'description'\")\n```\n\nThe end of the generated output looks like this.\n\n```\n## ⚠️ Validation warnings\n- foo-agent.md: missing 'description'\n```\n\nNote\n\nFiles that are missing frontmatter entirely get routed to the warnings as`\"missing frontmatter\"`\n\nand 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.\n\nThe StartCalendarInterval in `~/Library/LaunchAgents/com.shun.agents-index.plist`\n\nlooks like this.\n\n```\n<key>StartCalendarInterval</key>\n<dict>\n    <key>Hour</key>\n    <integer>6</integer>\n    <key>Minute</key>\n    <integer>30</integer>\n    <key>Weekday</key>\n    <integer>0</integer>\n</dict>\n```\n\n`Weekday: 0`\n\nis Sunday. The idea is that an up-to-date list is always waiting for me before I start work at the beginning of the week.\n\nProcess priority is lowered three ways: `Nice: 10`\n\n+ `LowPriorityIO: true`\n\n+ `ProcessType: Background`\n\n. Index generation involves disk I/O, but lowering the priority of the work keeps it from affecting Claude Code's own responsiveness.\n\nLoading is a one-time `launchctl load`\n\n.\n\n```\nlaunchctl load ~/Library/LaunchAgents/com.shun.agents-index.plist\n```\n\nIf you want to run it right now by hand, just call it directly.\n\n```\n~/.claude/scripts/agents-index.sh --json\n# → agents=27 warnings=0 → ~/.claude/agents/INDEX.md\n```\n\n`if p.name == \"INDEX.md\": continue`\n\n. Forget it and INDEX ends up reading INDEX and breaking`|`\n\nin a description breaks the table`.replace(\"|\", \"\\\\|\")`\n\nafter parsing. It's in the real code, but I forgot it at first`tools`\n\narrives as a JSON array string, so it easily exceeds the 60-character limit`[\"Read\", \"Write\", \"Edit\", \"Bash\", \"Grep\", \"Glob\"]`\n\nis already close to 50 characters. Mixing in MCP tools overflows immediately, so truncation + `...`\n\nis essential`>>`\n\nredirect in ProgramArguments in the plist gives you duplicate logs`pmset -a wake 1`\n\n`~/.claude/agents/`\n\ngrows, a manually maintained INDEX.md is guaranteed to rot → automate the generation`.index.json`\n\n`Weekday: 0 / Hour: 6 / Minute: 30`\n\nruns it automatically at 6:30 on Sundays. `Nice: 10`\n\n+ `LowPriorityIO`\n\nkeeps it out of your way`agents-index.sh --json`\n\nNext time, I plan to write about a mechanism that uses this agent list to visualize cost trends per model.\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/auto-generating-an-index-of-your-claude-code-custom-agents-from-their", "canonical_source": "https://dev.to/bokuwalily/auto-generating-an-index-of-your-claude-code-custom-agents-from-their-frontmatter-hk3", "published_at": "2026-07-29 00:00:06+00:00", "updated_at": "2026-07-29 00:31:44.472777+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "generative-ai"], "entities": ["Claude Code", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/auto-generating-an-index-of-your-claude-code-custom-agents-from-their", "markdown": "https://wpnews.pro/news/auto-generating-an-index-of-your-claude-code-custom-agents-from-their.md", "text": "https://wpnews.pro/news/auto-generating-an-index-of-your-claude-code-custom-agents-from-their.txt", "jsonld": "https://wpnews.pro/news/auto-generating-an-index-of-your-claude-code-custom-agents-from-their.jsonld"}}