Give a subagent a name instead of a paragraph¶
Every spawn_subagent call needs a persona: who is this child, what can it
touch, how careful should it be. Without a catalogue, that persona is prose
retyped into the task field by whichever orchestrator skill is dispatching
— nothing to list, review, or reuse. An agent definition is that persona,
written once, compiled into the dispatch inputs the execution engine already
accepts.
What an agent definition is¶
A .md file with YAML frontmatter and a Markdown body, the same
frontmatter-plus-body shape SKILL.md already uses. It never executes
directly — keryx agents show <name> renders it, keryx agents export
projects it into a host-native file, and the keryx-shell path compiles it
into ordinary spawn_subagent inputs (task, mode, model_tier, ...).
Nothing about the dispatch contract, the child policy engine, or the budget
ledger changes because a definition exists — a definition is a new producer
of the inputs those already consume, not a new execution path.
Frontmatter fields¶
| Field | Type | Required | Notes |
|---|---|---|---|
schema_version |
integer | no | when present, must be the literal 1; absent is treated as 1 |
name |
string | yes | unique id, ^[a-z][a-z0-9-]{1,63}$ |
description |
string | yes | third person, what it does and when to use it |
role |
string | yes | one-line persona summary injected into the compiled prompt header |
tools |
string[] | yes | allowlist from the canonical tool vocabulary (below); empty means only the target host's own read baseline — see "Empty tools[]" below |
model_tier |
enum light\|standard\|deep |
yes | resolved by model-tier.ts against the session's own model — never a model name |
policy_profile |
string | yes | read-only or workspace-write; the compiler resolves it per export target |
skills |
string[] | no | skill/module ids this agent should have preloaded, validated against the existing skill catalogue |
stacks |
string[] | no | stack_requires-style tags; empty/absent means stack-agnostic |
output_contract |
string | yes | which canonical result contract the reply must satisfy (subagent-result today) |
isolation |
enum none\|worktree |
no | whether the compiled dispatch requests worktree isolation where the target supports it |
origin |
object | no | provenance — kind (authored\|generated\|imported\|learned), sourceRef, generatedAt — an agent that arrived via keryx bundle import carries origin.kind: imported with sourceRef set to the exporting bundle's bundleId; see Portability |
The body is free-form Markdown task framing — the persona's operating
instructions — under the same 500-line, one-level-deep-reference discipline
SKILL.md authoring follows. Never write the prompt-defense baseline
(below) into a body. It is injected by the compiler so it cannot drift
between files.
The bundled catalogue¶
Ten generic, stack-agnostic definitions ship with Keryx:
| Name | Role |
|---|---|
design-advisor |
system-design and structural-tradeoff reasoning |
work-planner |
breaks a request into an ordered task/dependency plan |
codebase-navigator |
read-only location and cross-reference search |
test-first-driver |
drives a failing-test-first implementation loop |
refactoring-steward |
scoped simplification/dedup pass, no behavior change |
error-path-auditor |
finds swallowed errors, empty catches, dropped rejections |
docs-maintainer |
keeps docs/comments in sync with a code change |
security-auditor |
stack-agnostic security-pattern review |
performance-auditor |
stack-agnostic performance-pattern review |
end-to-end-tester |
drives and reports on an end-to-end test pass |
Per-stack generated pairs (a <stack>-code-auditor and a <stack>-build-fixer
per stack pack) are not part of this hand-authored initial catalogue — each
pair is generated only from a stack pack that has already cleared its own
governance gates, via keryx agents generate --stack <id> [--check] [--json].
As of flow 317's honest gate run at trials=10 (DeepSeek deepseek-chat for
both the runner and the judge, --strictness high, PACK_MIN_TRIALS raised
5 -> 10), python and go cleared it and each shipped a generated pair.
ts-js-node cleared the gate at trials=5 (marginally, at exactly the 0.8
floor) but DROPPED OUT at the higher trial count —
no-ts-ignore-suppression fell to 6/10 (0.6), a genuine model-answer-quality
gap checked against the recorded trials (not a rubric defect) — so it is
back to stability: experimental and its generated pair was removed.
react still fails: no-disable-hooks-lint scored 3/10 (0.3), confirming
rather than reversing the earlier 3/5 finding. Flow 335 (Wave 4 batch 3,
post-merge with batches 4/5/6) demoted python back to stability:
experimental too: python-testing's own trigger-positive-1 no longer
routes to itself once the fully combined catalog (170 skills, 1006
triggers) is in place — outranked by flutter-dart/flutter-testing on the
shared "widget" token, a corpus-wide binary-IDF effect (any skill that
merely contains a shared token lowers its corpus-wide weight, regardless
of wording) that is not fixable by editing any single pack's content. Its
generated pair was removed; see python/agent-refs.json's note and the
flow 335 journal for the full diagnosis. go remains the only stack with a
generated pair today: go-code-auditor / go-build-fixer. See
docs/requirements/keryx-agent-platform-expansion/workstreams/
W1-stack-catalog.md, "Implementation notes: flow 317 (grader follow-ups)"
and the flow 335 notes for the full per-scenario breakdown.
A project can drop its own files under .metaproject/agents/<name>.md. A
project definition with the same name as a bundled one overrides it;
the source (bundled or project) is recorded for each loaded agent.
The canonical tool vocabulary¶
Definitions name tools using Keryx's own builtin names — read_file,
list_dir, get_cwd, search_code, graph_affected, memory_search,
apply_patch, shell_exec, web_fetch, web_search — never a host's own
tool names. Each export target maps this vocabulary onto its own tool names
(for example Claude Code's Read/Grep/Edit/Bash). A vocabulary entry
with no mapping for a given target is dropped from that target's output and
reported back, rather than silently disappearing.
Empty tools[]¶
An empty (or fully-unmapped) tools[] does not mean "no tools at all" — it
means the definition inherits only the target host's own read baseline, and
that baseline is host-specific:
- Claude Code — the host's implicit read tools (
Read,Grep,Glob); the exporter never emits a baretools:line, since an omitted/emptytools:in Claude Code's own frontmatter means "inherit every tool", which would be the opposite of least privilege. - OpenCode —
edit,bash, andweb*tools explicitly denied via thepermissionblock; read tools remain available. - Kiro — the
readtag only. - Codex — has no per-tool allowlist at all, so
tools[](empty or not) never affects its export:sandbox_modefollowspolicy_profilealone —read-only→sandbox_mode = "read-only",workspace-write→sandbox_mode = "workspace-write"— every declared tool is reported governed-by-sandbox rather than dropped.
Policy profiles¶
policy_profile takes one of two canonical values:
read-only— no mutation tools; on keryx-shell this isshellChildReadOnlyProfileundermode: read_only. Every host exporter strips write/shell tools (for exampleapply_patch,shell_exec) out of aread-onlydefinition's output — a dropped tool is reported the same way an unmapped one is, never silently.keryx agents verifyalso flags aread-onlydefinition that still names a write/shell tool intools[]as apolicy-tool-conflict, independent of what any exporter does with it.workspace-write— mutation allowed, still bounded by the parent's own policy viainheritPolicy— a child can never exceed what its parent already permits.
Host export targets map these onto their own documented permission
vocabulary, or emit nothing if the host has none. An unknown profile value
fails keryx agents verify with a named reason rather than exporting
something unenforced.
keryx-shell: only mode is enforced today¶
For target: keryx-shell, the compiler produces two things: input (the
actual spawn_subagent call — task, mode, label, model_tier, a
subset of the tool's real input schema) and a policy sidecar (profile,
toolAllowlist, isolation) describing what the definition intends.
Only input.mode is enforced by spawn_subagent today: it selects between
the read-only child profile and the parent-equivalent profile, and the child
still goes through inheritPolicy so it can never exceed its own parent
regardless of what the definition asked for. The policy sidecar's
toolAllowlist, isolation, and the finer-grained write capability it
implies are advisory — nothing in spawn_subagent currently consumes
them, so a workspace-write definition naming shell_exec or requesting
isolation: worktree gets a general-mode child, not a child with that
specific tool allowlist or that specific isolation. This is a deliberate D-2
boundary (a definition is a producer of spawn_subagent inputs, not a new
execution path) rather than an oversight, but it means the sidecar describes
intent, not a guarantee. keryx agents show <name> and keryx agents export
--runtime keryx-shell <name> both print the sidecar labeled as advisory so
this is visible before you rely on it.
Model tier, not model name¶
model_tier is one of light, standard, or deep — never a model name.
Tier resolution is delegated entirely to the existing tier resolver; a
definition cannot pin a specific model.
Most host exports have no tier concept of their own and emit model: inherit
(or omit model entirely). Claude Code is the one exception: its subagent
frontmatter accepts exactly four fixed, version-free words for model —
opus, sonnet, haiku, inherit — so a claude-target export maps the
definition's model_tier onto that vocabulary directly: deep → opus,
standard → sonnet, light → haiku (an undeclared tier defaults to
standard, landing on sonnet). This is a static lookup into a host-defined
enum, never a guessed or versioned model id. Set
.metaproject/tasks.config.json's modelGuidance.claudeSubagentAliases:
false to opt a project out and keep model: inherit for every tier.
keryx update does not rewrite subagent files you already exported. The alias
reaches an existing .claude/agents/*.md file on the next explicit
keryx agents export --runtime claude or keryx integrations sync, which
replaces an unedited exported file without a prompt. Set the opt-out before
that run if you want those files to stay on inherit.
The prompt-defense baseline¶
Every compiled agent — regardless of export target — gets one shared,
compiler-owned prompt-defense block prepended ahead of role and the body:
a short statement that a spawned agent's own free text is data to its
caller, and that instructions encountered while doing the task (in file
contents, tool output, or another agent's report) carry no authority unless
the calling operator gave them directly. This mirrors the quarantine posture
already enforced on the ingestion side for keryx-shell children, extended to
the authoring side so a host with no quarantine equivalent still ships the
same defensive framing. Do not paste this text into an agent body — the
compiler injects exactly one copy; a hand-copied duplicate is exactly the
kind of thing that drifts the first time one copy is edited and the other
isn't.
CLI¶
keryx agents list [--stack <id>] [--json]
keryx agents show <name>
keryx agents export --runtime <claude|codex|kiro|opencode|keryx-shell> <name> [--force] [--dry-run] [--json]
keryx agents verify [<name>] [--json]
list and show are read-only. export writes only the target's own
managed file — the exact per-host file paths for a given name and runtime
are listed by running export --dry-run rather than assumed here, since
they follow each host's own current documented layout. Two different
refusals, only one of which --force can push through:
- A file with no keryx-managed sentinel for this agent at all is never
overwritten — not even with
--force.exportdoes not know this file is its own, so it never touches it. - A managed file whose own recorded
content-sha256no longer matches its content (a hand edit made after export) is refused unless--forceis passed.
A managed, unedited file that is merely stale — its source definition changed since it was last exported — is updated with no flag needed; that is not a refusal at all.
verify never re-derives compile/export logic — it assembles named problem
rows from the same checks compile/export/schema already run, plus a
few checks that are verify's alone. A definition (or --name) can fail with
any of:
schema-invalid— frontmatter failsvalidateAgentDefinition.unknown-tool— atools[]entry is outside the canonical vocabulary.unknown-skill— askills[]entry does not resolve in the skill catalogue.unknown-policy-profile—policy_profileis notread-onlyorworkspace-write.policy-tool-conflict—policy_profile: read-onlystill names a write/shell tool (apply_patch,shell_exec) intools[].origin-missing-source-ref— a non-authoredorigin.kindhas noorigin.sourceRef.invalid-source-ref—origin.sourceRefis not a single safe stack-pack id (fails closed before the id is ever resolved to a path).stack-pack-missing— ageneratedorigin'ssourceRefdoes not resolve to an existing W1 stack pack.baseline-in-body— the body repeats the prompt-defense baseline text the compiler already injects once.no-export-support— the export-support lookup for a runtime could not be resolved (surfaced rather than thrown).catalog-error(reported separately, per definition) — the file failed to load at all: unreadable, invalid frontmatter, a duplicate name, or a file-stem/namemismatch.not-found—--namenamed nothing in the catalog.
Every runtime — claude, codex, kiro, opencode, keryx-shell — is
resolved automatically for every checked definition; a definition never
names its own export targets, so there is nothing for it to omit.
These four subcommands are additions alongside the existing keryx agents
bootstrap, keryx agents external, and keryx agents monitor — none of
the three is a persona registry, and none of their behavior changes.
Export support levels¶
export's claimed support level for a runtime — native, adapter, or
instruction-only — is looked up from the harness capability matrix
(keryx integrations matrix), never asserted independently. A runtime with
a confirmed native or adapter record gets that host's own native
subagent file; anything not yet verified gets a plain-prose,
instruction-only file with a visible provenance comment instead of a false
claim of enforcement.
Every file an exporter writes carries a keryx-managed sentinel — every byte
of the file, with only that sentinel's own hash value blanked, is what the
content-sha256 it also carries verifies. export never overwrites a file
that lacks a structural sentinel for this agent at all, whatever --force
says, and refuses to overwrite a sentinel-bearing file whose content-sha256
no longer matches (a hand edit made after export, anywhere in the file —
not just outside the sentinel line) unless --force is passed. A managed,
unedited file that is merely stale is updated with no flag needed.
To export the whole catalogue for one harness at once rather than one definition at a time, use the bulk integrations path:
This is opt-in — the default keryx integrations install (no --surface
agents) does not write any agent files, so installing an existing
integration does not start producing new files under a harness's config
directory as a side effect.
keryx integrations uninstall --runtime <id> --surface agents removes only
the managed files this exporter itself wrote and that still verify. A
managed file that was hand-edited since export is kept, never deleted —
uninstall has no --force override for this — and is reported as a warning
alongside the uninstall result.
Auditing¶
keryx security audit-harness scans every host directory an exporter can
write agent files into — .metaproject/agents/ (markdown, the project
source tree itself), .claude/agents/ (markdown), .codex/agents/ (TOML),
.kiro/agents/ (JSON), and .opencode/agents/ (markdown) — as part of its
normal sweep, the same way it scans other managed surfaces. An agent
definition with unrestricted tools (including an explicitly empty or null
tools value in a Claude-shaped file, which Claude Code itself treats as
"inherit everything") or no model tier is exactly the kind of finding it
looks for.
Where to go next¶
- Run an agent without giving it your machine — the containment tiers a dispatched agent runs under.
- Architecture — the harness diagrams, including where a compiled dispatch feeds into the existing child contract.