# Extending Zero Zero is an open-source terminal coding agent. Out of the box it does the obvious things — read, edit, run, search — but the design point of the project is that **every surface is configurable**. This document is the user-facing guide for that configuration. If you only want to *use* Zero, the [README](../README.md) is enough. This page is for the other three jobs: 1. Tell the agent about *your* project (drop an `AGENTS.md` in your repo). 2. Add new specialist sub-agents. 3. Wire Zero into the rest of your toolchain (MCP, skills, hooks, plugins). ## 1. Drop a project `AGENTS.md` When Zero starts in a directory, it looks for project-level instructions and injects them into the system prompt. The lookup walks from your current working directory **up to the nearest git root** and reads the first matching file at each level — general rules at the repo root, more specific rules in sub-trees. Files are labeled with their directory in the prompt (e.g. `## Project guidelines (services/api/AGENTS.md)`). Accepted file names, in priority order at each level: | Path | Notes | | --- | --- | | `./AGENTS.md` | The classic spot — committed to your repo, shared with the team. | | `./ZERO.md` | Brand-specific alias. Same format, lower priority. | | `./.zero/AGENTS.md` | Project-local, hidden, gitignored. Personal notes that stay out of git. | Matching is **case-insensitive** on the basename, so `AGENTS.md`, `Agents.md`, and `agents.md` resolve to the same file on Windows and macOS. The git-tracked filename in this repo is `AGENTS.md` — keep that on case-sensitive filesystems (Linux, the WSL filesystem, or a CI runner) to match what the loader looks for. Both files use the same format. YAML frontmatter is optional; the markdown body is loaded as instructions for the agent. Zero reads the file once at session start, so changes take effect on the next `zero` launch — not mid-session. ```markdown # Project conventions for - Build with `make`, not `go build` directly. - Tests live next to the source file (`foo_test.go` next to `foo.go`). - Run `make lint` before opening a PR. - Never edit files under `third_party/` — those are vendored. ``` Tips: - Keep each file under ~8 KiB. Zero caps the **total** across all matched files at 32 KiB; everything past the cap is dropped. - Re-state rules in the imperative voice: "Run `make lint`", not "you should consider running the linter". - Don't put secrets, model IDs, or environment-specific paths in `AGENTS.md`. Use `config.json` for those. - In a monorepo, drop a narrower `AGENTS.md` in each sub-tree (e.g. `services/api/AGENTS.md`). Zero picks those up automatically when you launch from inside the sub-tree. - A YAML frontmatter block (`---\n...\n---`) at the top is preserved verbatim in the injected prompt but is not parsed for `globs:` or `alwaysApply:` scoping today — keep the body self-contained. ### Personal guidelines, across every project For preferences that follow *you*, not a specific repo (tone, tooling habits, workflow), drop a `ZERO.md` in your user config directory: `~/.config/zero/ZERO.md` on Linux/macOS, `%AppData%\zero\ZERO.md` on Windows — the same directory as `config.json` and your personal specialists. Same format and 8 KiB cap as the project files above, and the same case-insensitive basename match. This file is injected as its own `## User guidelines` section, before the project's `AGENTS.md`/`ZERO.md`, and is labeled as personal preference in the prompt: project guidelines are the later, more specific instruction and take precedence over it when the two conflict. ## 2. Custom specialists Specialists are Zero's sub-agents. Three scopes, in priority order: | Scope | Path | Shared? | | --- | --- | --- | | Built-in | compiled into Zero | yes — `worker`, `explorer`, `code-review` | | User | `~/.config/zero/specialists/*.md` | no — your machine only | | Project | `./.zero/specialists/*.md` | yes — the repo team | Project overrides user overrides built-in when names collide. A specialist is a markdown manifest with frontmatter and a system prompt: ```markdown --- description: Reviews API changes for breaking-change risk and missing tests. tools: read-only,plan --- You review API changes. For every changed hunk in `internal/api/` or any file that ends in `_api.go`: 1. Confirm the public signature is backward-compatible, or note the breaking change explicitly with the migration path. 2. Confirm a corresponding test exists in `internal/api/*_test.go` and that the new behaviour is exercised. 3. Flag any new exported symbol without a doc comment. Reply with one JSON object per finding: `{"file", "line", "severity", "message", "fix"}`. ``` CLI management (the prompt is passed inline via `--prompt`): ```bash zero specialist list zero specialist show api-reviewer zero specialist create api-reviewer \ --project \ --description "Reviews API changes" \ --tools read-only,plan \ --prompt "$(cat api-reviewer.md)" zero specialist edit api-reviewer --project zero specialist delete api-reviewer --project zero specialist path # prints the resolved specialists directory ``` The full format spec (frontmatter fields, tool scopes, prompt conventions) is in [`docs/SPECIALISTS.md`](SPECIALISTS.md). > **Roadmap.** An in-UI specialist manager (create / edit / delete / preview) is on the backlog. Today you use the `zero specialist` CLI subcommands above. ## 3. Skills Skills are markdown instruction packs the agent can pull in on demand. Each skill is a directory containing a `SKILL.md`. Standalone project skill directories are not supported in this version (shared project-wide skills must go in `AGENTS.md` or as a hook). However, project plugins (section 6) may bundle skills, which are merged into the active run. Discovery roots (earlier wins on name collisions): 1. **Primary Zero dir** — `$ZERO_SKILLS_DIR` if set, else `$XDG_DATA_HOME/zero/skills`, else `~/.local/share/zero/skills/` 2. **Shared multi-agent dir** — `~/.agents/skills/` when present (read-only discovery; never an install target) 3. **Plugin skill roots** — skills bundled by active plugins (section 6) A missing directory is fine — Zero just omits it. Management commands (`zero skill add` / `zero skill remove`) always write to the primary Zero dir only; `list` / `info` search primary + `~/.agents/skills`. `skill info` reports lock source/hash when present and whether the on-disk `SKILL.md` has drifted from the recorded hash. For a skill installed since Zero started installing bundled files, `skill info` also reports drift in any file installed with it. ```text ~/.local/share/zero/skills/ # primary (install/remove target) run-benchmarks/ SKILL.md write-changelog/ SKILL.md ~/.agents/skills/ # optional shared multi-agent root shared-review/ SKILL.md ``` `SKILL.md` format: ```markdown --- description: Run the project's benchmark suite and summarize the deltas. --- # Run benchmarks 1. `make bench` — captures the wall-clock and RSS before and after. 2. `benchstat before.txt after.txt` — diffs the two. 3. Report any regression > 5% with the function name and the previous value. ``` Only `name` and `description` are recognized in the frontmatter today. The `name` defaults to the directory name. Within a single skills root, duplicate names are resolved by lexicographic directory order. Across roots, Zero loads the primary directory first, then `~/.agents/skills`, then plugin skill roots; earlier roots win name collisions. Plugin-declared skills (section 6) are merged into the active agent run at plugin activation time, so bundled skills appear in the available skills list and can be loaded with the `skill` tool. The `skill` tool lets the agent load any discovered skill by name (primary, agents, or plugin). A skill can ship files beside its `SKILL.md`, in the [agentskills.io](https://agentskills.io/specification) layout: `scripts/`, `references/`, `assets/` or anything else the instructions refer to. `zero skill add` installs the whole skill directory, leaving out hidden entries such as `.git` and not following or copying symlinks (it lists any it skipped). A skill larger than 1,000 files or 32 MiB is refused. When the `skill` tool loads a skill that has such files, its output ends with the skill's directory and a list of the files, so instructions like "run `scripts/fill.py`" can be followed. Running a bundled script goes through the normal tool permissions; installing or loading a skill never runs anything. ```text pdf/ SKILL.md scripts/fill.py references/form-fields.md ``` ## 4. Hooks Hooks fire shell commands on lifecycle events. Configure them in JSON: - User: `~/.config/zero/hooks.json` - Project: `./.zero/hooks.json` ```json { "enabled": true, "hooks": [ { "id": "block-rm-rf", "event": "beforeTool", "matcher": "bash", "command": "/usr/local/bin/zero-hook-block-rmrf.sh", "enabled": true }, { "id": "log-session", "event": "sessionStart", "command": "/usr/local/bin/zero-hook-log.sh", "enabled": true } ] } ``` The `args` array (when present) is passed verbatim to `exec.CommandContext`. The actual hook payload — event name, matcher, tool call id, tool name, tool input, tool output, status — is delivered to the command as **JSON on stdin**, not via `${...}` substitution. A typical handler reads stdin and decides what to do: ```bash #!/usr/bin/env bash # /usr/local/bin/zero-hook-block-rmrf.sh set -euo pipefail payload="$(cat)" if printf '%s' "$payload" | grep -q '"input":"[^"]*rm[[:space:]]+-rf'; then echo "refusing rm -rf" >&2 exit 1 fi ``` Events the agent emits (in dispatch order): | Event | Fires when | Matcher allowed? | | --- | --- | --- | | `beforeTool` | A tool is about to run | yes (tool name) | | `afterTool` | A tool just returned | yes (tool name) | | `sessionStart` | A session begins | no | | `sessionEnd` | A session ends | no | | `specialistStart` | A sub-agent is spawned | yes (specialist name) | | `specialistStop` | A sub-agent ends | yes (specialist name) | For swarm members the matcher subject is the **agent type** (`subagent`, `teammate`, …), not a registered specialist profile name — swarm members run from an inline manifest keyed by agent type. `specialistStart` fires once the child process has actually started; a launch that fails before exec (missing binary, spawn error) records no public lifecycle hooks. `specialistStop` fires once per terminal stop: completion, error, explicit stop of a recovered task, and a background task reaped by a later `TaskOutput` all share one durable claim, so a hook that notifies or cleans up does not run twice for the same sub-agent run. A hook's exit code decides what happens next: `0` continues, non-zero blocks the tool call (`beforeTool`) or surfaces an error (`afterTool`). `sessionStart` / `sessionEnd` / `specialistStart` / `specialistStop` hooks are advisory (failures are audited but do not interrupt the run). Hook execution is recorded in the audit log; the audit is reachable from the agent's view of past actions, not from a dedicated `zero doctor` check. > **Roadmap.** An in-UI hooks manager is on the backlog. Today you edit the JSON directly. ## 5. MCP — Model Context Protocol Zero is both an **MCP client** (it can call external MCP servers) and an **MCP server** (other agents can call its tools). ### As a client — configure MCP servers in `config.json` ```json { "mcp": { "servers": { "docs": { "type": "stdio", "command": "docs-mcp", "args": ["--port", "7777"] }, "github": { "type": "http", "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN_HERE" } } } } } ``` Manage via CLI: ```bash zero mcp add docs --type stdio -- docs-mcp --port 7777 zero mcp add github --type http --url https://api.example.com/mcp \ --header "Authorization=Bearer YOUR_TOKEN_HERE" zero mcp list zero mcp check docs zero mcp remove github zero mcp oauth login github ``` Servers are merged from user and project configs (project wins on conflicts). Token-bearing values in `config.json` are sent verbatim — there is no `${env:...}` expansion — so prefer one of: - A wrapper script that sources the secret and execs the real command. - A `--header` value produced by command substitution (`"Authorization=Bearer $(print-token)"`) in a private shell config that you keep out of git. - A secret manager that injects the env var your MCP server reads on its own (the `command` and `args` then run inside that environment). Project MCP config remains supported for shared team servers, but a project layer that changes an existing server's target (`url`, `command`, or `args`) does not inherit user credentials. Clear or replace `headers`, `env`, or `oauth` explicitly, or use a new server name. OAuth tokens are bound to the resolved server identity, so changing a server target can require `zero mcp oauth login ` again. OAuth server names ending in `.<32 hex chars>` are reserved for token storage. #### Built-in servers Zero seeds two MCP servers before your config is merged. Both show up in `zero mcp list`, and any field can be overridden in your user config. - `exa` (`https://mcp.exa.ai/mcp`) is on by default for keyless web search. `zero mcp disable exa` turns it off. - `aimlapi` (`https://mcp.aimlapi.com/mcp`, the AI/ML API Toolbox, model discovery, image/video/audio generation, chat, embeddings and balance tools) is **off by default**, because its tool runs and generations are billed to your AI/ML API balance. Turn it on with `zero mcp enable aimlapi`, which writes only `"disabled": false` to your user config, and off again with `zero mcp disable aimlapi`. A project config cannot turn it on. When `aimlapi` connects, Zero sends your AI/ML API key as `Authorization: Bearer `. The key is resolved again on every request and is never written to `config.json`. With an aimlapi.com provider profile (`zero providers setup aimlapi`), the key is chosen in this order: 1. An inline key on the profile (if set in config). 2. Otherwise the environment variable named by the profile's `apiKeyEnv` (the catalog default is `AIMLAPI_API_KEY`). 3. Otherwise the key held in the credential store when the profile uses `apiKeyStored`. When there is no aimlapi profile, Zero uses `AIMLAPI_API_KEY` alone. If both `AIMLAPI_API_KEY` and a stored profile key exist, step 2 wins over the store whenever the inline key is empty — MCP requests may bill the environment key's account, not the stored key. No automatic key is sent when an aimlapi profile, or any profile reading `AIMLAPI_API_KEY`, uses a base URL other than `https://api.aimlapi.com/v1`, or when `AIMLAPI_INFERENCE_URL` is overridden, because that credential may belong to a proxy. No key is sent when the resolved value is not a single token (for example a comma- or newline-separated pool), when you set your own non-empty `Authorization` header, when the server uses `"auth": "oauth"`, or when project config touched the server. Without a key the server answers 401. To sign in with your aimlapi.com account instead, set `"auth": "oauth"` on the server and run `zero mcp oauth login aimlapi`. Every request to exactly `https://mcp.aimlapi.com` (https, default port, whatever the server is named, including one you add by hand) also carries the attribution headers `X-AIMLAPI-Source: agent/zero` and `X-AIMLAPI-Partner-ID`, and gets the key under the rules above. Other MCP servers never receive these headers or the key. If the server echoes back the key Zero sent it, Zero replaces the key with `[REDACTED]` in that server's error messages and tool results before anything reads them, and reports an HTTP error status without the response body. ### As a server — expose Zero's tools to another agent ```bash zero serve --mcp zero serve --mcp -C /path/to/workspace zero serve --mcp --add-dir /path/to/extra ``` The server speaks MCP over stdio. Configure it from the receiving side as a `stdio` server whose command is `zero serve --mcp`. `-C/--cwd` sets the workspace root exposed as MCP resources and used by core tools. `--add-dir` (repeatable) widens that resource/tool scope beyond the workspace without granting the sandbox temp roots used by interactive runs. ## 6. Plugins A plugin is a self-contained directory that bundles tools, hooks, and skills for one capability. Plugins live at: - User: `~/.config/zero/plugins//` - Project: `./.zero/plugins//` Each plugin has a `plugin.json` manifest: ```json { "id": "github-pr-review", "name": "GitHub PR Review", "description": "Adds review tools for GitHub PRs.", "version": "1.0.0", "tools": [ { "name": "list_prs", "command": "./tools/list_prs.sh" } ], "hooks": [ { "name": "pre-merge-check", "event": "beforeTool", "command": "./hooks/pre-merge.sh" } ], "skills": [ { "path": "./skills/review-checklist/SKILL.md" } ] } ``` Install and manage: ```bash zero plugins add ./github-pr-review # copy into ~/.config/zero/plugins/ or ./.zero/plugins/ zero plugins list zero plugins info github-pr-review # show manifest path, enabled state, lock metadata zero plugins disable github-pr-review # sets "enabled": false in plugin.json zero plugins enable github-pr-review # sets "enabled": true in plugin.json zero plugins remove github-pr-review # alias: rm ``` A plugin is active when it is present under a plugins directory and its `plugin.json` has `"enabled": true` (the default when the field is omitted). Use `zero plugins disable ` / `zero plugins enable ` to toggle that field without removing the plugin. Pass `--user` to target only user plugins and ignore `./.zero/plugins`; without it, a project plugin with the same id takes precedence. The toggle edits the `enabled` value in place — the rest of the manifest keeps its formatting, field order, and numeric literals — and serializes with `plugins add` / `plugins remove` on the same plugins root. Zero reads plugins when it starts, so the change applies to the next `zero exec` run or interactive session; an interactive session that is already running keeps the plugin tools, hooks, and skills it started with until you restart it. Plugin commands run with the plugin directory as their working directory. Use relative paths; the loader resolves them at activation time. > **Roadmap.** An in-UI plugins manager (browse, install, enable / disable) is on the backlog. Today you use the `zero plugins` CLI subcommands above. ## 7. Configuration locations Five configuration sources, in precedence order (later sources override earlier ones): | Source | Path / Key | Notes | | --- | --- | --- | | Built-in defaults | compiled in | Lowest priority. | | User config | `~/.config/zero/config.json` | Your machine. Never committed. | | Project config | `./.zero/config.json` | The repo. Committed (or not, your call). | | Environment | `ZERO_*` | Provider commands, secrets, skills dir override. | | CLI flags | `--model`, `--mode`, ... | Highest priority, per-invocation. | The user config holds things that should follow the user across projects (default provider, default model, theme). The project config holds things the team agreed on (provider catalog, sandbox policies, model restrictions). The sandbox `additionalWriteRoots` key is **ignored in project config** by design — a checked-out repo cannot widen its own sandbox. Set it in the user config or pass `--add-dir` per-invocation. ## 8. End-to-end example A team that wants every contributor's Zero to behave the same way commits: - `AGENTS.md` — project conventions, build commands, do-not-edit lists. - `.zero/config.json` — provider catalog, default model, allowed tools. - `.zero/specialists/api-reviewer.md` — the team's PR-review specialist. - `.zero/hooks.json` — block `rm -rf` on `beforeTool`. - `.zero/plugins/internal-tooling/` — a plugin that adds the team's internal CLI tools to the agent's toolset. Each contributor adds only: - `~/.config/zero/config.json` — their personal API keys, theme, default mode. - `~/.config/zero/ZERO.md` — personal preferences that follow them across every project (see section 1). - `~/.local/share/zero/skills/` — personal skills they keep across projects. That's it. Run `zero` from the repo root and the agent has the team's full instruction set, every contributor's personal setup, and nothing else. ## 9. Reference - [README](../README.md) — install, quickstart, command reference. - [docs/SPECIALISTS.md](SPECIALISTS.md) — full specialist manifest spec. - [docs/STREAM_JSON_PROTOCOL.md](STREAM_JSON_PROTOCOL.md) — `zero exec` I/O contract. - [docs/INSTALL.md](INSTALL.md) — install from source or release.