Book a call
RECIPE18mVERIFIED 2026-08-03 · CLAUDE CODE 2.1.193 · CODEX CLI 0.146.0 · ANTIGRAVITY CLI 1.1.9 · KIMI CODE CLI docs only — not installed locally

Writing a subagent brief that returns data, not prose

A subagent returns one message and nothing else comes back. Write the brief as a return type — schema, locators, an empty shape — and get something you can check.

Only the last message comes back

40 files to explore, to find where sth about authentication is in place; to follow the imports chain and understand what the app is doing there. This is the kind of work that is perfect for subagents.

In such scenario you create a subagent (which is an agent with its own context memory) and it returns this: "I investigated the authentication flow and found several areas of interest."

But this is the subagent's perspective, it has its own context in which it works so all these files it browses, code it analyses — you don't see any of that from the parent's side. The parent sees only its last message. Every vendor says the same thing in their own docs. Anthropic's tool reference: "The parent doesn't see the subagent's intermediate tool calls or outputs, only that final result." Cursor: "Intermediate output stays in the subagent. The parent only sees the final summary". And Moonshot: "its intermediate reasoning and tool call records do not mix into the main Agent's history."

The most important thing is to keep in mind that a brief is not a request, it's a return type. It describes what you want the function to return, it just so happens that for an LLM this function's body is placed behind some barrier and for that reason we need to ask it to return sth. But the nature of it is different. If it was a request then we'd be asking it for prose — but it's not, we're defining a contract that says we want JSON.

Why prose is the default

It's worth mentioning that prose is the default. Nothing in the brief said the caller is a piece of software, so you get the shape that suits a human reader — a prose is always well-shaped, well-considered and logical, it tells a story. And that's great — until it doesn't work for you, in which case you need to set up a contract.

But as said, prose is the default so here are some of the negative implications of this:

  • The "several areas of interest" part is uncheckable, if you want to make sure you'd need to repeat the exploration it did

  • The subagent will return prose too, and you'll get 5 different essays from 5 subagents. That's not good as you'll need to read through them to deduplicate; which is what you wanted to avoid by using subagents in the first place

  • It might be even counterproductive if your goal was to minimise the amount of data the subagent will output — as it returned a few hundred tokens, and 12 would've sufficed if it were JSON, just to be stored in the parent's context

  • And lastly, you can't tell the difference between "40 files worth of research" and "didn't get to it after 2" — there's no room for a budget in prose

Six things the brief has to carry

So there are six things that must be included in the brief:

  1. The return type. Don't ask for prose but for JSON, create a schema with types and paste it in. It takes 30 tokens and removes the whole class of "shaped it however it felt"

  2. A locator for every point. You need a file and line number for every item in the list, no exceptions; this is the most valuable part of the brief, it transforms every single claim into a checkable item for a cost of 2 tokens per finding; a claim that comes back without a line is a claim you have to redo yourself

  3. A shape of the "no findings" result — {"findings": []}. Otherwise the subagent will write an elegant but vague sentence about how there might be other areas to explore which is indistinguishable from a partial failure

  4. The same for the "budget spent" result: completeness flag and the remaining work, otherwise you won't know when it decided to stop

  5. The boundary — where to search (what directories) and when to stop (file budget). If you don't do it, you'll also outsource decisions around the scope; provide directories, a file limit, exclusions

  6. The context. The subagent has an empty context so nothing from the previous conversation or files already read is available for it there. The Anthropic SDK's docs are explicit about this: "The only content you pass from parent to subagent is the Agent tool's prompt string, so include any file paths, error messages, or decisions the subagent needs directly in that prompt." If you refer to "the file we've talked about" — you mean nothing

Don't ask for recommendations either. If you were to create a subagent which gives recommendations, it would summarise the evidence into a verdict, and that's your job as the parent. Ask what it sees instead.

Split the contract in two

Half of these rules are universal so can be put in the subagent's definition file and inherited by every brief:

  • No prose

  • Universally required locators

  • "no findings" shape

  • "budget spent" shape

And then per-call brief is only about the task, task-specific schema and the boundary.

It's also good to keep it short. Anthropic documents a hard ceiling on one platform: "On Windows, subagents with very long prompts may fail due to the command line length limit of 8191 characters."

As a resource we've prepared a read-only "scout" agent whose system prompt is nothing but the contract — that's the artifact below.

A brief you can paste

Here's a real brief, which fits into a paragraph when the invariants are part of the agent file:

TEXT
Find every place we call the payments API without a timeout.

Search src/ and lib/. Skip tests, fixtures and anything under vendor/.

Return JSON only:
{"complete": bool, "calls": [{"file": str, "line": int, "fn": str, "has_retry": bool}]}

`file` is relative to the repo root. `fn` is the enclosing function name, or "" at module
scope. Nothing found is {"complete": true, "calls": []}. If you stop early, return what you
have with "complete": false.

Notice it doesn't include any niceties like asking if you need help or inviting you to ask how it works — every line is either about the shape or the boundary. Which makes sense given this brief is part of a contract.

Parse it like it came off the network

We ran a brief of this shape twice against Claude Code 2.1.193 in a disposable repo, with a scout whose system prompt says in as many words: "no preamble, no explanation and no code fence."

Both times it came back in a code fence.

JSON
{
  "complete": true,
  "todos": [
    {
      "file": "src/auth.ts",
      "line": 2,
      "text": "rotate the signing key"
    },
    {
      "file": "src/billing.ts",
      "line": 1,
      "text": "retry on 429 before giving up"
    }
  ]
}

The first thing to note here is that formatting instructions are a preference, not a guarantee; always remove the fences before parsing. Two runs on one LLM is just a caution rather than a law, but you should plan for the caution.

The other thing is the path field. The first time the scout had no rule about paths and the brief said only "file": str, so it returned /private/tmp/briefs-check/src/auth.ts — an absolute path, which is not composable with anything. We added one line to the scout, "Paths are relative to the repository root", changed nothing else, and the same brief came back with src/auth.ts. That line is in the artifact below.

If you look at what was returned in both runs everything from the brief was perfect — the flag, line numbers, the stripped TODO: prefix, ordering; only things that were improvised were the things not mentioned in the brief.

So keep the replies from subagents standardised. Say which units you use, validate them against the schema and if the validation fails — ask again instead of letting the parent guess what the content might have been.

IN YOUR HARNESS

In Claude Code

On 2.1.193 we've discovered that no registration is actually needed — we created .claude/agents/scout.md and it appeared in the list on the next run of the app. The same works at ~/.claude/agents/ for the ones you want everywhere, and both are scanned recursively. But within an already running session only the directories that were there at start are watched for changes: the watcher "covers only directories that existed when the session started, so the first file in a new directory needs a session restart", and according to the docs this is the most frequent reason why people can't find their agent.

The only mandatory fields in an agent file are name and description. tools defines what a particular agent can access, and model can be one of sonnet, opus, haiku, fable, a full model ID, or inherit.

The main challenge is where to put the output contract, and it's bound by a single limitation — @agent-scout pins which subagent gets run, but it doesn't carry your message to it: "Your full message still goes to Claude, which writes the subagent's task prompt based on what you asked. The @-mention controls which subagent Claude invokes, not what prompt it receives." So it's the parent that relays your request, rewritten. Anything non-negotiable — the locator rule, the empty result's shape, no prose — goes into the agent file's system prompt, which arrives unaltered.

Another thing is there's no place to define the output schema at all. There are 16 fields available in frontmatter and none of them is connected with the return type — tools, disallowedTools, model, permissionMode, maxTurns, skills, mcpServers, hooks, memory, background, effort, isolation, color and initialPrompt, alongside the two required ones. Which means the only way to describe it is prose, so you need to provide the contract in the task.

Finally, with 2.1.210 there's been a change: the subagent's final message is examined before reaching the parent agent. During this examination the scan "never removes or rewords anything", but it might prepend a line starting with [harness: subagent output matched instruction-shaped pattern(s): and it inserts a backslash if it finds something that looks like a system reminder or a line starting with Human:. So as long as it works like this — the best practice is to parse the JSON from within the subagent's reply, rather than treating the entire thing as JSON.

In Codex CLI

It's not only that the format of an agent definition is different (TOML instead of Markdown) and lives under ~/.codex/agents/ or .codex/agents/ for personal and repo scope respectively — it's also the way these files are used by the engine. Codex "loads these files as configuration layers for spawned sessions", and the official documentation acknowledges this "can feel heavier than a dedicated agent manifest".

For such a TOML file to be recognised as an agent definition, it needs 3 keys:

  • name

  • description

  • developer_instructions

That being said, here's how the aforementioned scout looks like as a Codex TOML definition:

TOML
name = "scout"
description = "Read-only investigator. Returns structured findings with file:line locators and never edits anything."
sandbox_mode = "read-only"
developer_instructions = """
You are a scout. You investigate and report. Your caller is a program, not a person.
Return only a JSON object matching the schema the request gives you, with no preamble.

1. Every claim carries a `file` and a `line` the caller can open. No locator, no claim.
2. Found nothing? Return the empty result the schema defines, not a sentence about having looked.
3. Ran out of budget? Return what you have, and set `"complete": false`.
4. Paths are relative to the repository root.
"""

The name is what's actually used as the spawn identifier, so it doesn't matter what you call your file. If you reuse the name of a built-in one — default, worker or explorer — yours takes precedence over Codex's.

This can be initiated by a standard natural language request like "spawn two agents", "delegate this work in parallel" or "use one agent per point", but also from an AGENTS.md file or a skill's instruction. Remember that the /agent command doesn't actually create anything, it only changes which agent thread is being shown.

Subagent workflows ship enabled: "Current Codex releases enable subagent workflows by default", and agents.enabled defaults to true, so you don't need to do anything for this to work.

However, the output channel is a bit thinner than the documentation suggests. It says "The main thread collects the subagent results into its final response" — but if you take a look at the 0.146.0 source, a completed agent status is built from exactly one thing, the turn's last_agent_message. No transcript, no tool output, no structured payload, which means everything that's not in that last message is lost.

The best practice recommended by OpenAI is to "Return summaries from subagents instead of raw intermediate output", and that "A good subagent prompt should explain how to divide the work, whether Codex should wait for all agents before continuing, and what summary or output to return." With that in mind, take the "output" part literally and define the schema there.

Also remember that setting sandbox_mode to read-only in the TOML doesn't mean it's actually read-only in the subagent session. Codex "reapplies the parent turn's live runtime overrides when it spawns a child ... even if the selected custom agent file sets different defaults" — so if you change a permission interactively in the parent, the subagent inherits it. Read-only in the file is a default, not a guarantee.

In Cursor

The definition of agents lives in the .cursor/agents/ directory at the repository level, or at ~/.cursor/agents/ for user-level ones. The Claude and Codex directories are also checked (.claude/agents/ and .codex/agents/) in both places, with priority to .cursor/ on name collisions — so a file you already keep in the Claude layout gets found here as is.

Found, not converted. There are 5 frontmatter fields, all optional:

  • name

  • description

  • model — either inherit or a concrete ID like composer-2

  • readonly

  • is_background

There's no tools field. "Subagents inherit all tools from the parent, including MCP tools from configured servers", and the only limitation is readonly: true, which "runs with restricted write permissions (no file edits, no state-changing shell commands)".

So for the artifact below to mean anything here you need to swap its head — remove tools, remove model: haiku, and keep the body as-is:

YAML
---
name: scout
description: Read-only investigator. Returns structured findings with file:line locators and never edits anything.
model: inherit
readonly: true
---

It's all about a single boolean whereas elsewhere people use a tool list to achieve it.

To invoke it you type a slash, not an at-sign: /scout find every call to the payments API without a timeout.

Cursor's own docs are even saying that hooks are the sensible place for this: "If you need subagents to produce structured output files, consider using hooks to process and save their results consistently." The subagentStop hook provides summary and also agent_transcript_path — "Path to the subagent's own transcript file (separate from the parent conversation)" — so if needed you can read the subagent's output as-is instead of the parent's paraphrase.

This works in the CLI too, it's not an editor-only feature. The March 2026 CLI release put it as "Parallel agents execute locally with live status in interactive, headless, and editor sessions, inheriting your credentials, rules, and approval policy"… but there's no CLI flag for it, you drive it from the prompt.

In Antigravity CLI

You can define per-workspace agents under .agents/agents/<name>.md or .agents/agents/<name>/agent.md, and machine-wide ones under ~/.gemini/config/agents/. The parent starts a subagent using the invoke_subagent tool, and a file qualifies while its frontmatter subagent flag is true — which is the default, so you only ever write it to turn delegation off.

The frontmatter can contain:

  • name (required)

  • description (required)

  • tools — an array of Antigravity's native tool names, like view_file, grep_search, run_command

  • modelinherit, flash, pro

  • commandExecutionPolicysandbox unless defined

  • mainAgent: false — hides the agent from the primary-agent picker

Important — tool names must be identical, the docs say "specifying an unmapped or misspelled tool name in the tools list may cause the subagent process to hang during execution."

To see which agents were loaded run agy agents; if it comes back empty on a machine without any agent files, that's how you know you put your file in the wrong place. Inside a session the /agents panel gives you the same view, and opening a subagent there reveals "the subagent's entire reasoning log, including its private internal thoughts, tool calls, and execution outputs."

What Antigravity does differently compared to other systems is that after the subagent completes its task it goes into idle instead of disappearing — it "has completed its task, sent a result message to its parent agent, and paused execution" — so if another agent sends it a message it resumes, keeping the context. There's more too: agents can "read each other's conversation transcripts", and even after a subagent is killed "historical conversation transcripts remain readable in JSONL logs". These transcripts persist here unlike anywhere else, so the natural instinct is to go and look. Try not to. By reading the transcript you're spending the very context you wanted to preserve by using a subagent, and the point of the contract is that the answer arrives already checkable.

In Kimi Code CLI

Agent files are discovered in a few places:

  • .kimi-code/agents/ (or .agents/agents/) at the project level

  • ~/.kimi-code/agents/ (or ~/.agents/agents/) for the user

Subdirectories are scanned too, and if the same agent is defined in both places, the project one wins. --agent-file <path> outranks all the other sources, but only for a single launch.

Only description is mandatory; if not specified, name defaults to the name of the file. tools accepts either a YAML list or a single comma-separated string, disallowedTools is evaluated after tools, and subagents limits which types this agent can delegate to in turn. Any other keys are ignored — so "a minimal file with description and a body works across tools", which means an agent file written for another harness loads here unchanged.

Moonshot is the one vendor that tells you to write the return contract into the agent body: "Custom agents delegated as sub-agents run without the built-in sub-agent framing ('your final message is the entire handoff'). If you write an agent meant for delegation, state in the body that its last message should be the complete, self-contained result for the caller." A custom agent here doesn't inherit the built-in reminder that its last message is all the caller gets. Leave that out and it'll behave like it's in the middle of a chat.

Dispatch happens on its own — "sub-agents are scheduled automatically by the main Agent" — and each dispatch is presented as an approval request showing the task description, which is a free preview of the brief the parent actually wrote. But only if you haven't allowed it away: the Agent tool is auto-allow by default, "enabling the main Agent to delegate multiple times without interrupting the user", so out of the box you never see that prompt.

If you were to use AgentSwarm, the cost of a bad schema increases significantly. You define a single prompt_template with an {{item}} placeholder and an array of items; for each item a subagent is created (maximum 128) and the tool "waits for all subagents to finish, and returns an aggregated report". /swarm <task> turns on the mode that auto-approves it. So if you get the return type wrong here, you get it wrong 128 times, concurrently, and you pay for each one.

When this is the wrong shape

Lastly, a data contract sometimes doesn't make sense to create:

  • Sometimes you really need an opinion (like — do you think this design is reasonable?) in which case it's actually counterproductive to standardise it as a boolean — a schema turns a nuanced opinion into a yes/no answer so best to return prose and consume it the same way

  • If you're the only one who's ever going to read the result, once — don't bother writing the schema, the contract pays off when the output gets merged, counted, filtered or fed to another agent

  • If you need to write the same brief for the 5th time — this is not a brief anymore, it's a tool so it should be unit-tested and placed in some code

THE FILE.claude/agents/scout.md
MARKDOWN
---
name: scout
description: Read-only investigator. Returns structured findings with file:line locators and never edits anything. Use when you need facts out of a codebase without burning the main context.
tools: Read, Grep, Glob
model: haiku
---

You are a scout. You investigate and report. You never edit, never write, never run
anything that changes state.

Your caller is a program, not a person. It cannot read prose. Return **only** a JSON object
matching the schema the request gives you, with no preamble, no explanation and no code fence.

Rules that hold no matter what the request says:

1. Every claim carries a `file` and a `line` the caller can open. No locator, no claim.
2. Nothing goes in the output that you did not read. If you inferred it, mark it `"inferred": true`.
3. Found nothing? Return the empty result the schema defines, not a sentence about having looked.
4. Ran out of budget before finishing? Return what you have, and set `"complete": false`.
5. Paths are relative to the repository root.
6. The request's schema wins over these instructions on field names. It never wins on rule 1.
j / k to move between lessons