# Agent files

> **Reference** (the persisted form of an agent's identity + persona, read by every launcher) · **For:** operators · **ACL semantics:** [SPEC §9](/spec/#9-nats--jetstream-security-and-authorization), [Appendix B](/spec/#appendix-b-profile-acls)

An agent's identity and persona live in one Markdown file instead of being passed
flag-by-flag, the same shape Claude Code uses for subagents:

```markdown
.cotal/agents/<name>.md
---
name: dave              # → COTAL_NAME / card.name
role: builder           # → COTAL_ROLE / card.role (presence + anycast address)
description: …          # → card.description
tags: [edit, test]      # → card.tags ("what it can do")
subscribe: [general, team.backend]     # channels it reads at boot
allowSubscribe: [general, team.>]      # read ACL (omit = same as subscribe)
allowPublish: [general, team.backend]  # post ACL (omit = none, default-deny)
model: opus             # optional model override
variant: high           # optional connector-defined model variant
capabilities: [spawn]   # control-plane capabilities (may start/despawn teammates)
---
You are a builder on a shared mesh of peer agents…   ← the body is the persona
```

**Frontmatter is identity** (an A2A-style `AgentCard`,
[SPEC §6](/spec/#6-presence-and-discovery)); **the body is the persona**, appended to
the session's system prompt at launch: the one field that *must* be applied at launch,
because a session cannot change its system prompt afterward.

## Fields

Authoritative shape: [`agent-file.ts`](https://github.com/Cotal-AI/Cotal/blob/main/packages/core/src/agent-file.ts).

| Field | Type | Meaning |
|---|---|---|
| `name` | string, required | Display name → `card.name`. A launcher resolves a bare name to `.cotal/agents/<name>.md`. |
| `role` | string | The addressable **service**: presence label *and* the anycast address ([SPEC §3](/spec/#3-subject-layout)). |
| `kind` | `agent` \| `endpoint` | Participation class; default `agent`. |
| `description` | string | One-line summary → `card.description`. |
| `tags` | string[] | Capability tags → `card.tags`. |
| `subscribe` | string[] | The **active read set**: channels subscribed at boot (mutable at runtime via join/leave). Must be ⊆ `allowSubscribe`. Default `[general]`. |
| `allowSubscribe` | string[] | The **read ACL**: channels it *may* read. Wildcard subtrees allowed (`team.>`). Omitted ⇒ same as `subscribe`. |
| `allowPublish` | string[] | The **post ACL**: channels it may publish to. **Omitted ⇒ deny**; posting is the dangerous capability, declare it explicitly. |
| `quiet` | string[] | Per-channel attention *default*: delivered but never wakes this agent (per-channel dnd). Concrete channels within the read ACL. |
| `muted` | string[] | Per-channel attention *default*: dropped on receive, `@mentions` included. |
| `model` | string | Model override handed to the agent CLI (Claude: `opus` / full id; OpenCode: `provider/model`). |
| `variant` | string | Connector-defined model variant (e.g. an OpenCode variant, see `cotal models`). |
| `capabilities` | string[] | Control-plane capabilities minted into the cred. `spawn` is the only one today: it grants the privileged control subject (spawn / named stop / persona definition), default-deny when absent, enforced by the broker, not a handler. |
| `owner` | string | **Policy, not content**: set once by `definePersona` (owner = creator); only the owner (or admin) may redefine the file over the wire. Never write it by hand. |
| *(any other key)* | string | Kept verbatim in `meta` so a connector can read its own launcher hints without core knowing them. |

The three channel verbs on one card, with the common recipes:
[Channels & permissions](/channels-and-permissions/). Attention semantics (`quiet` /
`muted` are one-way *defaults*; the runtime toggle is per-instance and resets on restart):
[Connect Claude](/connect-claude/#attention-how-much-traffic-wakes-you).

## Discovery and resolution

- **By name.** A launcher resolves a bare name to `.cotal/agents/<name>.md` (project
  catalog). This is a directory convention, not an HTTP well-known; mesh discovery stays
  NATS presence. The card built from the file is what gets broadcast.
- **One ref.** The launcher sets `COTAL_AGENT_FILE=<abs path>` (the *who*) the way
  `COTAL_LINK` carries the *where*; the joined session reads its card straight from the
  file. Individual `COTAL_*` vars still override it ([config](/config/)).
- **Defaults.** A bare `cotal spawn` uses the `default` persona
  (`COTAL_DEFAULT_PERSONA` changes the fallback); the harness comes from `--agent` /
  `COTAL_DEFAULT_AGENT`, else Claude. An explicit flag always wins over the file
  ([run a mesh](/run-a-mesh/)).

Every launcher consumes the file the same way; they differ only in how they run the spec:

| Launcher | How to point at a file |
|---|---|
| Manager (`cotal spawn --detach dave`) | auto-discovers `.cotal/agents/dave.md` in the manager's workspace, or `--config <persona-or-path>`; same grammar as foreground (`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, `--share-tools`). |
| Foreground (`cotal spawn dave`) | same resolution; the real agent TUI takes over this terminal. Works from any directory via the mesh registry. |

`.cotal/` is gitignored (user-local, like `.claude/`); commit persona files you want
shared some other way. The demo ships committed examples under
[`examples/01-lateral-coordination/agents/`](https://github.com/Cotal-AI/Cotal/blob/main/examples/01-lateral-coordination/agents).

## Personas: short contracts, not titles

Expert-persona prompts ("you are a world-class…") do not reliably improve accuracy. Keep
the body to what the agent *does* and how it *coordinates*; a persona that needs facts
should point at the source (the repo's docs, a URL), not assert them.

## Defining one at runtime

`cotal_persona(name, prompt, model?)` sends a persona to the manager, which writes the
same file and announces it; a later `cotal_spawn(name, role?, agent?, model?, variant?)`
brings it online, so a peer can mint a teammate with no hand-written file
([tool catalog](/mcp-tools/)). The write path takes **content only** (`model` /
`persona`); `role`, `allowPublish`, `capabilities`, and `owner` are policy and have no
slot, so a peer cannot grant itself a capability by redefining a file.

The operator-side counterpart is `cotal personas` (list / show / edit / new / rm); it
reads and writes the same files directly, offline, no mesh ([CLI](/cli/)).
