Home › Articles

Custom subagents in Claude Code, Cursor and Codex from one source

Updated 2026-11-03

In short: all three tools let you define a named helper agent with its own instructions and its own context window, but each stores it differently. Claude Code and Cursor use a Markdown file with YAML frontmatter, Codex uses a TOML file, and the fields only partly overlap. "Read-only" is a tool list in Claude Code, a readonly flag in Cursor, and in Codex only an instruction. Below: where each tool looks, what each field does, a tested script that writes all three files from one source, and the gotchas that make a subagent quietly not load.

Subagent or skill?

A skill adds instructions to the conversation you're in. A subagent is a separate worker: it starts with a fresh context, does the job, and returns a summary, so the search results, logs and file dumps stay out of your main thread. Claude Code's docs suggest one "when a side task would flood your main conversation with search results, logs, or file contents you won't reference again", and a custom subagent "when you keep spawning the same kind of worker with the same instructions". Cursor's docs make the same split: subagents for long research, parallel work and independent checks; skills for quick, single-purpose actions.

All three tools already ship built-in helpers (Claude Code's Explore and Plan, Cursor's explore and browser, Codex's default, worker and explorer). Write a custom one only when you need different instructions or limits. Good first candidates are a reviewer that can't edit, a test runner that reports only failures, and a researcher that reads docs.

Where each tool looks

Claude CodeCursorCodex
Project.claude/agents/*.md.cursor/agents/*.md, plus .claude/agents/ and .codex/agents/.codex/agents/*.toml
Personal~/.claude/agents/~/.cursor/agents/, ~/.claude/agents/, ~/.codex/agents/~/.codex/agents/
File formatMarkdown, YAML frontmatter, body = system promptMarkdown, YAML frontmatter, body = promptTOML, prompt in developer_instructions
Same name twiceProject beats personal; the nested .claude/agents/ closest to the working directory winsProject beats personal; .cursor/ beats .claude/ and .codex/A custom agent named like a built-in (e.g. explorer) replaces it
Run it yourselfName it in your prompt, @-mention it, or claude --agent name/name in the prompt, or name itAsk Codex to spawn it by name; /subagents in the CLI shows the threads

Claude Code also scans subfolders of .claude/agents/ and every .claude/agents/ from your working directory up to the repo root, and identifies agents by the name field, not the filename. Codex also uses the name field as the source of truth. Cursor falls back to the filename when name is missing.

Which fields each tool reads

What you wantClaude CodeCursorCodex
Required fieldsname, descriptionNone (name defaults to the filename)name, description, developer_instructions
When to delegatedescriptiondescriptiondescription (but see triggering below)
Modelmodel: sonnet, opus, haiku, fable, a full model ID, or inheritmodel: inherit (default) or a model IDmodel and model_reasoning_effort; leave both out to use the parent's
No file editstools allowlist (e.g. Read, Grep, Glob) or disallowedTools: Write, Editreadonly: true (no file edits, no state-changing shell commands)No field. The sandbox comes from the parent session
Run in the backgroundbackground: trueis_background: trueNot a per-agent field
ExtraspermissionMode, skills, mcpServers, hooks, memory, isolation: worktree, maxTurns, effort and moreModel parameters in brackets, e.g. claude-opus-5[effort=high]features.shell_tool = false and other features.* switches, skills.config to turn off skills

The only fields all three share are name and description, plus the prompt itself. Model names don't carry over either: sonnet is a Claude Code alias, and Cursor takes inherit or a model ID. In a Markdown file that both Claude Code and Cursor might read, model: inherit is the one value both document.

Gotcha 1: read-only isn't the same in each tool

Gotcha 2: Cursor reads Claude Code's folder too

Cursor loads .claude/agents/ for compatibility, so a repo with only Claude Code subagents already has them in Cursor. But Cursor's docs list only name, description, model, readonly and is_background. They don't say what Cursor does with Claude-only fields like tools, so don't count on a Claude tools allowlist making an agent read-only in Cursor. Put a Cursor copy in .cursor/agents/ with the same name and readonly: true: on a name clash Cursor uses the .cursor/ file.

Cursor also lists .codex/agents/, but its docs describe subagents only as Markdown files and don't say whether it reads Codex's TOML files. We couldn't confirm either way, so the script below writes a Cursor file regardless.

Gotcha 3: Codex waits to be asked

Claude Code and Cursor delegate on their own when a task matches a subagent's description, and both docs suggest phrases like "use proactively" to encourage it. Codex's docs say current local releases spawn subagents "when you ask directly or when applicable AGENTS.md or skill instructions request it." So for Codex, add the trigger to your instructions, for example in AGENTS.md:

After changing code, spawn the code-reviewer agent on the changed files and fix its Critical findings before you finish.

Each subagent does its own model and tool work, so all three docs warn that subagents use more tokens than a single agent. Start with two or three; Cursor's docs call 50+ vague subagents an anti-pattern, and Claude Code warns at startup when the combined descriptions of your custom subagents pass 15,000 tokens.

Gotcha 4: Claude Code skips broken files without telling you

Claude Code ignores frontmatter fields it doesn't recognize "without reporting an error", so disallowed-tools or maxturns just does nothing. It also skips the whole file, with no message in the session, when:

Run claude --debug and search the log, or check a folder with claude plugin validate .claude/agents (v2.1.233 or later; it doesn't flag a missing name). If you just created the agents folder, restart: a running session only watches folders that existed when it started.

One source, three files

Keep each subagent once in agents-src/, with only the fields that mean the same thing everywhere, and generate the three native files. The source format is deliberately small: name, description and an optional readonly, then the prompt.

agents-src/code-reviewer.md:

---
name: code-reviewer
description: Reviews changed code for bugs, security problems and missing tests, without editing anything. Use proactively after code changes, or when the user asks for a review of the current diff.
readonly: true
---

You are a senior code reviewer. You review; you never fix.

1. Read every file the parent agent names, and the code each change calls into.
2. Look for bugs, unhandled errors, security problems (secrets, injection, missing auth checks) and changed behavior without a test.
3. Skip style comments unless they hide a real bug.

Reply with findings grouped as Critical, Warning and Suggestion. Give file:line for each one and say how to fix it. If you find nothing, say so in one line.

scripts/sync_agents.py (Python 3, no packages needed):

#!/usr/bin/env python3
"""Generate Claude Code, Cursor and Codex subagent files from agents-src/*.md.

python3 scripts/sync_agents.py          write .claude/agents, .cursor/agents, .codex/agents
python3 scripts/sync_agents.py --check  exit 1 if any generated file is missing or out of date
"""
import json, pathlib, re, sys

ROOT = pathlib.Path(__file__).resolve().parent.parent  # repo root (script lives in scripts/)
SRC = ROOT / "agents-src"
KEYS = {"name", "description", "readonly"}
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
CLAUDE_READONLY_TOOLS = "Read, Grep, Glob"
CODEX_READONLY_NOTE = "Do not edit files or run commands that change files, git state or anything outside the repo."


def parse(path):
    text = path.read_text(encoding="utf-8")
    m = re.match(r"---\n(.*?)\n---\n(.*)", text, re.S)
    if not m:
        sys.exit(f"{path}: must start with a --- frontmatter block")
    meta = {}
    for line in m.group(1).splitlines():
        if not line.strip():
            continue
        key, sep, value = line.partition(":")
        key, value = key.strip(), value.strip()
        if not sep or key not in KEYS:
            sys.exit(f"{path}: unknown or malformed line {line!r} (allowed keys: {', '.join(sorted(KEYS))})")
        meta[key] = value.strip('"')
    for key in ("name", "description"):
        if not meta.get(key):
            sys.exit(f"{path}: missing {key}")
    if not NAME_RE.match(meta["name"]):
        sys.exit(f"{path}: name must be lowercase letters, digits and single hyphens")
    if meta.get("readonly", "false") not in ("true", "false"):
        sys.exit(f"{path}: readonly must be true or false")
    meta["readonly"] = meta.get("readonly") == "true"
    meta["body"] = m.group(2).strip() + "\n"
    return meta


def q(s):  # a double-quoted string that is valid in both YAML and TOML
    return json.dumps(s, ensure_ascii=True)


def claude(a):
    lines = ["---", f"name: {a['name']}", f"description: {q(a['description'])}", "model: inherit"]
    if a["readonly"]:
        lines.append(f"tools: {CLAUDE_READONLY_TOOLS}")
    return "\n".join(lines + ["---", "", a["body"]])


def cursor(a):
    lines = ["---", f"name: {a['name']}", f"description: {q(a['description'])}", "model: inherit"]
    if a["readonly"]:
        lines.append("readonly: true")
    return "\n".join(lines + ["---", "", a["body"]])


def codex(a):
    body = (CODEX_READONLY_NOTE + "\n\n" if a["readonly"] else "") + a["body"]
    instr = f"'''\n{body}'''" if "'''" not in body else q(body)
    return f"name = {q(a['name'])}\ndescription = {q(a['description'])}\ndeveloper_instructions = {instr}\n"


TARGETS = [(".claude/agents", ".md", claude), (".cursor/agents", ".md", cursor), (".codex/agents", ".toml", codex)]


def main():
    check = "--check" in sys.argv[1:]
    agents = [parse(p) for p in sorted(SRC.glob("*.md"))]
    if not agents:
        sys.exit(f"no agents in {SRC}")
    names = [a["name"] for a in agents]
    if len(set(names)) != len(names):
        sys.exit("duplicate agent names in agents-src/")
    stale = 0
    for folder, ext, render in TARGETS:
        out = ROOT / folder
        out.mkdir(parents=True, exist_ok=True)
        for a in agents:
            f, want = out / (a["name"] + ext), render(a)
            have = f.read_text(encoding="utf-8") if f.exists() else None
            if have == want:
                continue
            stale += 1
            if check:
                print(f"out of date: {f.relative_to(ROOT)}")
            else:
                f.write_text(want, encoding="utf-8")
                print(f"wrote {f.relative_to(ROOT)}")
        for f in sorted(out.glob("*" + ext)):
            if f.stem not in names:
                print(f"not in agents-src (left alone): {f.relative_to(ROOT)}")
    if check and stale:
        sys.exit(1)
    print(f"{len(agents)} agent(s), {'up to date' if not stale else 'updated' if not check else 'stale'}")


if __name__ == "__main__":
    main()

For each agent it writes:

It rejects unknown keys in the source, so a typo fails loudly instead of being ignored the way Claude Code ignores it, and it allows only lowercase letters, digits and single hyphens in names. Claude Code accepts those, Cursor's docs say to use lowercase letters and hyphens, and Codex's docs give no naming rule (its examples use underscores). It never deletes files: generated folders can hold hand-written agents, and the script just lists any it didn't write.

Run python3 scripts/sync_agents.py after editing a source file and commit the output. Add python3 scripts/sync_agents.py --check to CI or a pre-commit hook: it exits 1 and names the file when someone edited a generated copy by hand.

How we tested it: in a scratch git repo with two agents (this reviewer and a test runner) we ran the write and --check modes, ran it from a subfolder, hand-edited a generated file (caught, exit 1), added a stray file (listed, left alone), and fed it a misspelled key, an invalid name, a file with no frontmatter and a prompt containing '''. Every output parsed with PyYAML or Python's tomllib, had each tool's required fields, and carried the same prompt text. We didn't run the agents inside Claude Code, Cursor or Codex, so the first time, check that each tool lists the agent: @ typeahead in Claude Code, / in Cursor, or ask Codex to spawn it.

A ten-minute setup

  1. Pick one job that floods your context today, such as review or test runs, and write it as agents-src/<name>.md. Make the description say what it does and when to use it.
  2. Copy the script to scripts/sync_agents.py, run it, and commit agents-src/ plus the three generated folders.
  3. For Codex, add one line to AGENTS.md saying when to spawn the agent (Gotcha 3). If it must not write, run that session read-only.
  4. Ask each tool to use the agent once and confirm it ran: Claude Code shows the agent's name on the delegation row, Cursor shows a subagent task card, and Codex opens an agent thread.
  5. Add --check to CI so the copies can't drift.

The same one-source idea works for the other files these tools read differently: rules (AGENTS.md vs CLAUDE.md), MCP servers and hooks. If your subagents and skills are piling up, see what too many agent skills cost in context.

FAQ

Where do custom subagents go in Claude Code, Cursor and Codex?

Claude Code reads Markdown files from .claude/agents/ in the project and ~/.claude/agents/ for your own. Cursor reads .cursor/agents/, and also .claude/agents/ and .codex/agents/, plus the same folders in your home directory. Codex reads TOML files from .codex/agents/ in the project and ~/.codex/agents/.

Does Cursor use my Claude Code subagents?

Yes. Cursor lists .claude/agents/ and ~/.claude/agents/ as subagent locations for Claude compatibility. If a subagent with the same name also exists in .cursor/agents/, Cursor uses the .cursor/ one. Cursor documents only name, description, model, readonly and is_background, so Claude-only fields such as tools may not carry over.

Can a Codex custom agent be read-only?

Not on its own. Codex subagents inherit the parent session's sandbox and approval settings, and sandbox_mode or approval_policy in a custom agent file don't override them. The instructions can tell the agent not to edit, but Codex's docs say that doesn't create a separate read-only sandbox. Set the sandbox on the session you delegate from.

Why doesn't Claude Code load my subagent?

Claude Code silently skips an agent file with no name, no description, a --- line that isn't the first line, or YAML that doesn't parse, and it ignores misspelled fields without an error. Run claude --debug and read the debug log, or run claude plugin validate .claude/agents (Claude Code v2.1.233 or later). If the agents folder is new, restart the session.

Does Codex use subagents automatically?

Not by default. Codex's docs say current local releases delegate when you ask directly, or when AGENTS.md or a skill tells them to. Claude Code and Cursor delegate on their own based on each subagent's description, and both suggest words like "use proactively" in the description to encourage it.

Sources

Official docs, checked 2026-11-03.

Get the free rules sampler + new tips by email

Confirm your email and we'll send you straight to the sampler download. Then one short email every two weeks covering tool format changes (Cursor, Claude Code, Codex, Windsurf, Copilot), new rules and kit updates. No spam.

We store only your email, with our email provider Resend. See the privacy policy.

Unsubscribe anytime from any email. Rather skip email? Get the sampler on GitHub or download the zip directly.