Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/catalog-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
npm run sync:settings
npm run sync:env-vars
npm run sync:hooks
npm run sync:sub-agents

# `fetchedAt` is rewritten on every sync run — strip it before
# comparing so we don't open noise PRs that only bump a timestamp.
Expand All @@ -62,7 +63,7 @@ jobs:
set -euo pipefail
real_drift=false
drifted_files=()
for f in settings env-vars hooks; do
for f in settings env-vars hooks sub-agents; do
head_norm=$(git show HEAD:catalog/$f.json | jq 'del(.fetchedAt)')
new_norm=$(jq 'del(.fetchedAt)' catalog/$f.json)
if [ "$head_norm" != "$new_norm" ]; then
Expand Down Expand Up @@ -107,6 +108,7 @@ jobs:
- `npm run sync:settings`
- `npm run sync:env-vars`
- `npm run sync:hooks`
- `npm run sync:sub-agents`

Drifted catalogs:
```
Expand Down
87 changes: 87 additions & 0 deletions catalog/sub-agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"source": "https://code.claude.com/docs/en/sub-agents.md",
"fetchedAt": "2026-05-07T03:21:49.842Z",
"count": 16,
"fields": [
{
"name": "background",
"required": false,
"description": "Set to `true` to always run this subagent as a [background task](#run-subagents-in-foreground-or-background). Default: `false`"
},
{
"name": "color",
"required": false,
"description": "Display color for the subagent in the task list and transcript. Accepts `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, or `cyan`"
},
{
"name": "description",
"required": true,
"description": "When Claude should delegate to this subagent"
},
{
"name": "disallowedTools",
"required": false,
"description": "Tools to deny, removed from inherited or specified list"
},
{
"name": "effort",
"required": false,
"description": "Effort level when this subagent is active. Overrides the session effort level. Default: inherits from session. Options: `low`, `medium`, `high`, `xhigh`, `max`; available levels depend on the model"
},
{
"name": "hooks",
"required": false,
"description": "[Lifecycle hooks](#define-hooks-for-subagents) scoped to this subagent. Ignored for [plugin subagents](#choose-the-subagent-scope)"
},
{
"name": "initialPrompt",
"required": false,
"description": "Auto-submitted as the first user turn when this agent runs as the main session agent (via `--agent` or the `agent` setting). [Commands](/en/commands) and [skills](/en/skills) are processed. Prepended to any user-provided prompt"
},
{
"name": "isolation",
"required": false,
"description": "Set to `worktree` to run the subagent in a temporary [git worktree](/en/worktrees), giving it an isolated copy of the repository. The worktree is automatically cleaned up if the subagent makes no changes"
},
{
"name": "maxTurns",
"required": false,
"description": "Maximum number of agentic turns before the subagent stops"
},
{
"name": "mcpServers",
"required": false,
"description": "[MCP servers](/en/mcp) available to this subagent. Each entry is either a server name referencing an already-configured server (e.g., `\"slack\"`) or an inline definition with the server name as key and a full [MCP server config](/en/mcp#installing-mcp-servers) as value. Ignored for [plugin subagents](#choose-the-subagent-scope)"
},
{
"name": "memory",
"required": false,
"description": "[Persistent memory scope](#enable-persistent-memory): `user`, `project`, or `local`. Enables cross-session learning"
},
{
"name": "model",
"required": false,
"description": "[Model](#choose-a-model) to use: `sonnet`, `opus`, `haiku`, a full model ID (for example, `claude-opus-4-7`), or `inherit`. Defaults to `inherit`"
},
{
"name": "name",
"required": true,
"description": "Unique identifier using lowercase letters and hyphens"
},
{
"name": "permissionMode",
"required": false,
"description": "[Permission mode](#permission-modes): `default`, `acceptEdits`, `auto`, `dontAsk`, `bypassPermissions`, or `plan`. Ignored for [plugin subagents](#choose-the-subagent-scope)"
},
{
"name": "skills",
"required": false,
"description": "[Skills](/en/skills) to load into the subagent's context at startup. The full skill content is injected, not just made available for invocation. Subagents don't inherit skills from the parent conversation"
},
{
"name": "tools",
"required": false,
"description": "[Tools](#available-tools) the subagent can use. Inherits all tools if omitted"
}
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"sync:settings": "node scripts/sync-settings.js",
"sync:env-vars": "node scripts/sync-env-vars.js",
"sync:hooks": "node scripts/sync-hooks.js",
"sync:sub-agents": "node scripts/sync-sub-agents.js",
"test": "node --test scripts/*.test.js",
"test:coverage": "node --experimental-test-coverage --test-coverage-exclude='scripts/*.test.js' --test scripts/*.test.js",
"test:unit": "vitest run",
Expand Down
127 changes: 127 additions & 0 deletions scripts/sync-sub-agents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env node
// Fetches the Claude Code sub-agents docs page (markdown) and writes a
// reshaped catalog of supported frontmatter fields to
// catalog/sub-agents.json. Companion to sync-settings.js, sync-env-vars.js,
// and sync-hooks.js.
//
// What we parse: the `#### Supported frontmatter fields` table near the
// middle of the page, `| Field | Required | Description |`. That table
// enumerates the keys a `~/.claude/agents/<name>.md` (or project-scoped)
// definition file's YAML frontmatter accepts — `name`, `description`,
// `tools`, `disallowedTools`, `model`, `permissionMode`, `maxTurns`,
// `skills`, `mcpServers`, `hooks`, `memory`, `background`, `effort`,
// `isolation`, `color`, `initialPrompt`. The page also documents
// built-in subagents, model-resolution rules, hook semantics, and so on,
// but those live under prose-heavy sections; the frontmatter table is
// the smallest useful artifact for the catalog and parallels the shape
// of catalog/env-vars.json and catalog/hooks.json.
//
// Idempotence: re-running on unchanged upstream produces a one-line diff
// (fetchedAt only). Records are sorted by name.

import { writeFile, mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const SOURCE = 'https://code.claude.com/docs/en/sub-agents.md';

const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const OUT = resolve(SCRIPT_DIR, '..', 'catalog', 'sub-agents.json');

// Strip a single layer of wrapping backticks (`NAME` -> NAME). Leaves
// strings without wrapping backticks untouched.
function stripBackticks(s) {
const m = /^`(.+)`$/.exec(s);
return m ? m[1] : s;
}

// Parse a single GFM table row into {name, required, description}.
// Returns null for lines that don't look like a 3+ cell row.
//
// `required` is normalized to a boolean: "Yes" -> true, anything else
// (typically "No") -> false. Upstream uses exactly those two values.
export function parseRow(line) {
const parts = line.split('|');
// "| name | required | desc |" splits into
// ["", " name ", " required ", " desc ", ""].
if (parts.length < 5) return null;
const name = stripBackticks(parts[1].trim());
const requiredCell = parts[2].trim();
// Defensive join in case a stray pipe appears in the description.
const description = parts.slice(3, parts.length - 1).join('|').trim();
if (!name || !description) return null;
return {
name,
required: /^yes$/i.test(requiredCell),
description,
};
}

// Find the supported-frontmatter-fields table (header
// `| Field | Required | Description |`) and return its data rows.
// Returns [] if no such table exists in `markdown`. The page has other
// tables (e.g. the "Other" built-in subagents tab uses
// `| Agent | Model | When Claude uses it |`) — the header signature
// disambiguates.
export function parseTable(markdown) {
const lines = markdown.split('\n');
const rows = [];
let inTable = false;
for (const line of lines) {
if (!inTable) {
if (/^\|\s*Field\s*\|\s*Required\s*\|\s*Description\s*\|/i.test(line)) {
inTable = true;
}
continue;
}
// Skip the alignment row (`| :--- | :--- | :--- |`) right after the header.
if (/^\|\s*:?-+:?\s*\|/.test(line)) continue;
// First non-pipe line ends the table.
if (!line.startsWith('|')) break;
const row = parseRow(line);
if (row) rows.push(row);
}
return rows;
}

// Compose the full record list from raw markdown. Pure function: easy
// to drive from tests with a small fixture string.
export function buildRecords(markdown) {
const rows = parseTable(markdown);
rows.sort((a, b) => a.name.localeCompare(b.name));
return rows;
}

async function main() {
const res = await fetch(SOURCE);
if (!res.ok) {
throw new Error(`Fetch failed: ${SOURCE} → HTTP ${res.status} ${res.statusText}`);
}
const markdown = await res.text();

const fields = buildRecords(markdown);
if (fields.length === 0) {
throw new Error('Unexpected page shape: no `Field | Required | Description` table found');
}

const envelope = {
source: SOURCE,
fetchedAt: new Date().toISOString(),
count: fields.length,
fields,
};

await mkdir(dirname(OUT), { recursive: true });
await writeFile(OUT, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
console.log(`Wrote ${fields.length} subagent frontmatter fields → ${OUT}`);
}

const isMainModule =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;

if (isMainModule) {
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
}
Loading
Loading