> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ziro-agent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> How Ziro resolves per-agent policy files, what happens when one is missing, and where session state lives on disk.

Ziro has no single config file. Configuration is **per agent**: each agent owns a
folder of small YAML files, one file per concern. At startup Ziro resolves each
file independently, walking a fixed chain from most specific to least, and falls
back to built-in defaults when nothing supplies it.

## The per-agent folder

An agent is a directory whose name is the agent id. Everything the agent needs
lives inside it, so an agent is self-contained and copyable.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agents/
  default/
    meta.yaml               # name, description, enabled, model, provider
    agent_config.yaml       # persona / system prompt
    tool_policy.yaml        # core tools, search_k, max_active, denylist
    permissions.yaml        # allow / deny / ask globs
    guardrails_policy.yaml  # input + output guards
    compaction_policy.yaml  # context compaction
    mcp_servers.yaml        # external MCP servers
    fs_policy.yaml          # read_file / grep / glob_files / writes
    shell_policy.yaml       # run_shell
    websearch_policy.yaml   # web_search
    webfetch_policy.yaml    # web_fetch
    plugin_config.yaml      # which plugins this agent may surface
    ...
  registry.yaml             # holds only `default`: the agent used when none is named
```

`registry.yaml` is deliberately tiny: it names the default agent and nothing
else. Whether an agent is selectable at all is its own `meta.yaml` `enabled`
flag, not a central list.

## The resolution chain

Every file resolves **independently**, most-specific-first:

<Steps>
  <Step title="Project layer">
    `<project>/.ziro/agents/<id>/<file>`: this repo, this agent.
  </Step>

  <Step title="User layer">
    `~/.ziro/agents/<id>/<file>`: this machine, this agent. This is how you
    configure a pip-installed Ziro without editing the package.
  </Step>

  <Step title="Bundled package">
    The file shipped inside the `ziro` package.
  </Step>

  <Step title="Built-in defaults">
    No file anywhere: the loader constructs the config object from its
    dataclass defaults.
  </Step>
</Steps>

Two consequences worth internalising:

* **Per file, not per folder.** Overriding `permissions.yaml` in your project
  layer does not shadow the bundled `tool_policy.yaml`. Each file is looked up
  on its own, so you override exactly the concerns you care about.
* **Whole-file, not key-merge.** When a layer supplies a file, that file wins
  wholesale for that concern. Layers do not deep-merge key by key.

<Note>
  A missing file is normal and safe: it degrades to built-in defaults, it does
  not error. The one deliberate exception is `plugin_config.yaml`: an agent that
  no layer supplies it for surfaces **zero** plugins. See [Plugins](/guides/plugins).
</Note>

## The flavour overlay

A [flavour](/concepts/flavours) is a top-level container selected once at launch.
Beyond scoping which agents and threads you see, it can overlay a policy baseline
onto its member agents.

The overlay is merged **in memory, onto the parsed YAML, before the config object
is built**. A flavour never writes a policy file to disk. It merges two sources:

| Source                       | Scope                             |
| ---------------------------- | --------------------------------- |
| `shared_policies[<file>]`    | Every agent in the flavour        |
| `policies["<agent>/<file>"]` | One named agent (wins on a clash) |

So the full order for any one config value is:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
flavour per-agent overlay
  → flavour shared overlay
    → resolved YAML file (project → user → bundled)
      → built-in dataclass default
```

With no active flavour the overlay is empty and behaviour is byte-identical to
having no flavours at all.

The `engineering` flavour uses this to disable `web_search` and `web_fetch` for
its `coder` agent while leaving them on for the read-only `product_manager` and
`architect`. That is the segregation rule described in [Web tools](/guides/web).

## Policy file reference

| File                     | Purpose                                                                                                                 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `meta.yaml`              | Agent name, description, `enabled` flag, pinned `model` and `provider`, transport retry                                 |
| `agent_config.yaml`      | The persona / system prompt injected before every LLM call                                                              |
| `tool_policy.yaml`       | `core_tools` (always bound), `search_k`, `max_active`, `denylist`, per-turn tool-round budgets                          |
| `permissions.yaml`       | Tool gating: `default_action`, `deny` / `ask` / `allow` globs, `dangerous_default_deny`                                 |
| `guardrails_policy.yaml` | Toggles and configures the input and output guard nodes                                                                 |
| `compaction_policy.yaml` | Automatic context compaction: strategy, trigger, tool-output caps                                                       |
| `mcp_servers.yaml`       | External MCP servers and their transports                                                                               |
| `fs_policy.yaml`         | `read_file` / `grep` / `glob_files`, plus `allow_write` for `write_file` / `edit_file`; `deny_paths`, confinement, caps |
| `shell_policy.yaml`      | `run_shell`: enabled, host vs sandbox mode, interpreter, denylist, timeout, env passthrough                             |
| `websearch_policy.yaml`  | `web_search`: provider (`duckduckgo` or `searxng`), result caps                                                         |
| `webfetch_policy.yaml`   | `web_fetch`: enabled, SSRF guard, allowed schemes, content caps                                                         |
| `subagent_policy.yaml`   | Which children may be spawned, depth and spawn caps, parallelism, model defaults                                        |
| `memory_policy.yaml`     | Long-term memory extraction and reflection; project-memory settings                                                     |
| `hooks.yaml`             | Lifecycle hooks: event, matcher glob, dotted path or shell command                                                      |
| `handoff_policy.yaml`    | `request_handoff`: operator prompt, timeout behaviour, injection mode                                                   |
| `clarify_policy.yaml`    | `ask_user_question`: enabled, max questions and options                                                                 |
| `attachment_policy.yaml` | Image attachments: size limits, allowed types, notices                                                                  |
| `queue_policy.yaml`      | Background worker queue: workers, depth caps, result TTL                                                                |
| `voice_policy.yaml`      | Push-to-talk STT/TTS backends and capture settings                                                                      |
| `plugin_config.yaml`     | The policy ceiling for which plugins the agent may surface                                                              |
| `agents_md_policy.yaml`  | `AGENTS.md` ingestion: enabled, token cap, fallback filename                                                            |

Several of these gate registration entirely: when `shell_policy.yaml`,
`websearch_policy.yaml`, or `webfetch_policy.yaml` has `enabled: false`, the tool
is never registered at all. It is invisible to `search_tools` and to the subagent
permission view. The absence *is* the disable switch, so there is no separate
runtime check to bypass.

## Real examples

### `meta.yaml`

Identity, on/off state, and the provider/model pin.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
name: Generalist
description: Autonomous goal-driven generalist that discovers and uses whatever skills, tools, and subagents a task needs.
enabled: true
# Omit (or set to null) to use the provider's env default model instead.
model: minimax/minimax-m3
# Omit to use ZIRO_LLM_PROVIDER.
provider: openrouter

# Transport retry for transient provider failures (timeout / 429 / 5xx).
# Omit the block to use env / library defaults.
retry:
  enabled: true
  max_attempts: 3
  exponential_jitter: true
  base_delay: 1.0
  max_delay: 20.0
  max_elapsed: 60.0
```

### `permissions.yaml`

Precedence runs `dangerous_default_deny` → `deny` → `ask` → `allow` →
`default_action`. Note the shipped recipe for shell: it is allow-listed to lift
the dangerous default-deny, then `ask`-gated so every call still prompts.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
version: "1"
enabled: true
default_action: allow

dangerous_default_deny:
  - "shell:*"

deny:
  - "*:browser_run_code_unsafe"
  - "*:*delete*"

ask:
  - "fs_write:*"          # HITL every write_file / edit_file
  - "shell:run_shell"     # HITL every command (ask beats allow)

allow:
  - "rag:*"
  - "memory:*"
  - "tasks:*"
  - "shell:run_shell"     # lifts the shell:* dangerous-default-deny

remember_default_scope: thread
```

### `fs_policy.yaml`

`deny_paths` is a hard refusal, not a prompt. Any path component matching one of
these globs is refused for read and write, skipped by `grep`, and hidden from
`glob_files`. It keeps a prompt-injected agent away from secrets and away from
its own guardrail config.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
confine: true            # refuse `..` escapes outside the project root
respect_gitignore: true  # gitignored files may be listed but never read

deny_paths:
  - ".env"
  - ".env.*"
  - "*.pem"
  - "*.key"
  - "id_rsa"
  - "*_policy.yaml"
  - "permissions.yaml"
  - ".git"
  - ".ziro"

max_read_bytes: 256000
max_read_lines: 2000
max_grep_matches: 200
max_glob_results: 200

ignore_dirs: [".git", "node_modules", "__pycache__", ".venv", "dist", "build"]
```

## The three state files

Policy files describe intent and are yours to edit and commit. **State** files
record what happened and are written by Ziro. Do not hand-edit them.

<ResponseField name="~/.ziro/.env" type="layered env file">
  API keys, the default model, the LLM provider, Langfuse credentials, and
  `DATABASE_URL`. Written by the setup wizard (`ziro setup`) and by `/settings`.
  Read as layers, so a project-level `.env` can override it.
</ResponseField>

<ResponseField name="~/.ziro/users.json" type="MRU registry">
  The list of known user ids, `last_user`, and the last flavour each user chose.
  Drives the launch-time user picker and the `/user` command.
</ResponseField>

<ResponseField name="<DATA_DIR>/threads.json" type="per-workspace index">
  Thread metadata: user, agent, flavour, title, timestamps, and any per-thread
  LLM or plugin overrides. Checkpoints remain the source of truth for
  conversation *content*. This file is the index over them, and is rebuildable.
</ResponseField>

Two more per-project areas are keyed outside the repo by a stable workspace key
(`<basename>-<hash>`): project memory under `~/.ziro/projects/<key>/memory/` and
the per-thread scratchpad under your system temp directory. See
[Memory](/concepts/memory).

## Runtime mode

One environment variable decides the whole storage backend:

| `DATABASE_URL` | Store                                 | Checkpointer         | Vectors  |
| -------------- | ------------------------------------- | -------------------- | -------- |
| unset (dev)    | `SqliteStore` at `./data/memories.db` | `AsyncSqliteSaver`   | FAISS    |
| set (prod)     | `PostgresStore`                       | `AsyncPostgresSaver` | PGVector |

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose up -d   # PostgreSQL + pgvector for prod mode
```

## Inspecting and editing

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
python -m app.cli.manage_agents list                    # agents + on/off state
python -m app.cli.manage_agents add my_agent --name "My Agent"
python -m app.cli.manage_agents set-model my_agent anthropic/claude-sonnet-4
python -m app.cli.manage_agents disable my_agent
ziro setup                                              # re-run the setup wizard
```

In chat, `/settings` edits the API key, model, Langfuse, and database entries in
`~/.ziro/.env`, queuing a live rebind only for the section you actually changed.

## Next steps

<Columns cols={3}>
  <Card title="Agent file reference" icon="folder" href="/reference/agent-files">
    Every field of every policy file.
  </Card>

  <Card title="Environment variables" icon="key" href="/reference/environment">
    Every variable Ziro reads and where it is written.
  </Card>

  <Card title="Providers and models" icon="cpu" href="/guides/providers">
    Pinning a provider and model per agent.
  </Card>
</Columns>
