# GoClaw Configuration Guide

This guide covers everything you need to configure the GoClaw Multi-Agent Orchestration Platform. GoClaw uses a powerful YAML-based configuration system that supports hot reloading, environment variables, and secret interpolation.

---

## Configuration Methods

GoClaw can be configured using multiple methods:

1. **YAML Configuration File**: The primary configuration source: `config.yaml` in the state directory (`~/.goclaw/config.yaml`), else `config.yaml` in the working directory.
2. **Environment Variables**: Three override the file: `PORT` (`server.port`), `AI_PROVIDER` (`ai.provider`), and `DEBUG=1` or `DEBUG=true` (`debug`). `GOCLAW_STATE_DIR` sets the state directory, which is where `config.yaml` is read from (default `~/.goclaw`); `GOCLAW_HOME` replaces the home directory that `~/.goclaw` is under. `GOCLAW_SECRET_KEY` sets the key the secrets vault is encrypted with. Other settings are not read from the environment, except through a `${env:VAR}` reference in a credential field (see below); the few variables that switch on diagnostics or act as key fallbacks are listed in the [CLI reference](cli-reference.md#environment-variables).
3. **CLI Commands**: Direct manipulation of the YAML file via the CLI.
4. **Setup Wizard**: Interactive onboarding using `goclaw setup`. It opens by itself the first time `goclaw` starts with no `config.yaml`.

### The setup wizard

The wizard offers **QuickStart** and **Advanced**. QuickStart asks only what GoClaw cannot guess:

1. Which AI to use, and its API key. The key is checked with the provider straight away (a request for its list of models, which costs nothing): a refused key is asked for again, and one that could not be checked, with no network for example, is kept. For Ollama, QuickStart lists the models the local Ollama has pulled and asks which one to use.
2. What the agent is called and, optionally, what it helps with, in one sentence. This becomes the agent's `IDENTITY.md`. It is asked only while `IDENTITY.md` is still the file GoClaw seeded; after that, the persona is changed on the Web UI's **Instructions** page, where you can edit it as text or describe it in words and press **Draft**.
3. Optionally, a Discord bot token.

Everything else takes its default: the provider's model and endpoint, the workspace (`~/.goclaw/workspace`), minimal operating mode, port 8080 on `127.0.0.1`. Advanced asks for each of these (the model, a base URL, Vertex AI, the workspace, channels, the gateway) and also checks the key. When you change provider in either flow, the default agent follows it, unless its `model` names a model of its own.

### YAML Features
- **Hot Reload**: Changes to `config.yaml` are detected automatically. Some changes may require a restart, which will be logged.
- **References**: `${env:VAR}` (or `${VAR}`) reads an environment variable, `${secret:NAME}` a value from the encrypted vault (`goclaw secret set NAME <value>`), and `${file:/path}` the first line of a file. A bare `${VAR}` that is not set in the environment is also looked up in the vault. References are expanded only in credential fields: the `ai.*.api_key` keys, channel tokens and secrets (including `channels.msteams.app_id` and `tenant_id`, and `channels.facebook.verify_token`), `gateway.auth.token`, `gateway.ingress.authtoken`, `database.dsn`, `memory.graph.dsn` and `password`, datasource `password` and `dsn`, the tokens and passwords of `tools.observability`, `tools.gitlab`/`gitlabs` and `tools.sonarqube`/`sonarqubes` (also in an agent's `tools:`), MCP server `env` and `headers`, `logging.otel.dsn` and `headers`, the URL and secret of an admin hook's webhook, `tools.web_search` keys, voice and email credentials, and a webhook route's `secret`, `token` and `agent`. Anywhere else a `${...}` stays as written.
- **$include**: Split large configurations into multiple files using `$include: "path/to/extra.yaml"`, or a list of files:

  ```yaml
  channels:
    $include:
      - ./channels/discord.yaml
      - ./channels/msteams.yaml
  ```

  An included file's content is indented to the `$include` key's level; a relative path is relative to the file that includes it.
- **CLI commands** resolve `${secret:NAME}` the way the gateway does: `goclaw status`, `sessions`, `memory`, `message` and `tools exec` use the vault's value.

---

## CLI Configuration Management

You can get, set, append, remove or delete configuration keys directly from the terminal.

| Command | Description |
|---|---|
| `config set <key=value>` | Add or update a key-value pair. |
| `config get <key>` | Retrieve the current value of a key. |
| `config append <key=value>` | Add a value to a list. |
| `config remove <key=value>` | Remove a value from a list. |
| `config delete <key>` | Remove a key entirely, including one GoClaw does not recognise (the start-up log reports those as "unknown setting"). |

**Examples:**
```bash
# Set AI provider and API key
goclaw config set ai.provider=gemini
goclaw config set ai.gemini.api_key="AIzaSyA..."

# Add a skill to the allowed list
goclaw config append skills.allow=calculator

# View current provider
goclaw config get ai.provider
```

---

## LLM Providers

`ai:` says which model provider answers and holds each provider's credentials. Only the block of a
provider you use needs a key.

```yaml
ai:
  provider: "gemini"   # gemini | openai | claude | deepseek | ollama (default: openai)
  gemini:
    api_key: "${secret:GEMINI_API_KEY}"
    model: "gemini-2.5-flash"              # default
    # use_vertex: true                     # Vertex AI instead of an API key, with Google
    # project_id: "my-gcp-project"         # Cloud application default credentials
    # location: "us-central1"              # default
  openai:
    api_key: "${secret:OPENAI_API_KEY}"
    model: "gpt-4o"                        # default
    # base_url: "https://api.openai.com/v1"  # or any OpenAI-compatible endpoint
  anthropic:                               # the block for provider "claude"
    api_key: "${secret:ANTHROPIC_API_KEY}"
    model: "claude-sonnet-5-5"             # default
    # effort: "medium"                     # low, medium, high, xhigh, max; empty is the model's default
    # max_output_tokens: 16000             # default; thinking counts against it
  deepseek:
    api_key: "${secret:DEEPSEEK_API_KEY}"
    model: "deepseek-flash"                # default (reads images); or deepseek-v4-pro
    # base_url: "https://api.deepseek.com" # default
    # reasoning_effort: "high"             # none, low, high, max; empty is DeepSeek's default (high)
    # max_output_tokens: 16000             # default; thinking counts against it
  ollama:
    base_url: "http://localhost:11434"     # default
    model: "llama3"                        # default
```

- `ai.provider` is the default agent's provider. Claude's settings are under `anthropic`, but the
  provider is named `claude`. An unknown name stops the gateway at start-up with the list of valid
  ones. `gemini-legacy` is still accepted and served as `gemini`.
- An agent's `model` in `agents.list` (see [Multi-Agent Setup](#multi-agent-setup)) chooses its
  provider and model, and for the default agent it wins over `ai.provider`. Write
  `provider/model` (`ollama/llama3`, `openai/gpt-4o-mini`), a provider name alone (`gemini`: that
  provider's `model` above), or a model name that starts with `gemini`, `claude`, `deepseek`,
  `gpt-`, `o1` or `o3`. For other agents any other name is sent to the OpenAI client; for the
  default agent it is refused and the gateway does not start. Use the `provider/model` form.
- `ai.fallback` lists `provider/model` pairs tried in order when an agent's provider fails before
  the reply used a tool: a rate limit, an outage, a bad key or quota, a network failure. Each
  needs its credentials under `ai.<provider>`. An agent's own `fallback:` in `agents.list`
  replaces it (`[]` turns it off for that agent). A failure caused by the request itself (too
  long, malformed), or one after a tool ran, is not handed on. See
  [Provider Fallback](../architecture/provider_fallback.md). `goclaw doctor` and the dashboard's
  Doctor page show an `ai_fallback` card per agent: a warning when it has no fallback, or only
  one on its own provider (one outage then leaves it without replies and fails its SOP steps),
  an error when no fallback answers the same probe as the AI provider check.
- Token limits per session, agent and day are set under `ai.token_budget`, and
  each SOP run stops at 500,000 tokens unless `sop.max_run_tokens` says
  otherwise: see [Token Budgeting](token-budgeting.md).

---

## Gateway Authentication Token

The gateway does not start without `gateway.auth.token`; it stops with `CRITICAL: Gateway
authentication token is missing!`. `goclaw setup` writes a random token when you leave the field
blank.

```yaml
gateway:
  auth:
    token: "${secret:GATEWAY_TOKEN}"   # or "${env:GATEWAY_TOKEN}", or the token itself
```

Requests to `/api/*`, the `/gateway` WebSocket, `/v1/*` (OpenAI-compatible API), `/media/`,
`/uploads/` and `/canvas/` must carry it, in any of these forms:

- an `X-Goclaw-Token: <token>` header,
- an `Authorization: Bearer <token>` header,
- a `?token=<token>` query parameter (WebSocket and SSE clients),
- the `X-Goclaw-Token` cookie, which the one-time sign-in link in the start-up log (`Web UI: http://…/?auth_code=…`) sets.

Health probes (`/health`, `/ready`), the Web UI's own files, the App API (`/api/app/*`, which
checks its own credentials) and the webhook receivers (`/hook/*`, `/goclaw/hook/*`, which check
their own secrets) do not take it. When the token is a `${secret:…}` reference whose secret is not
in the vault, no token is accepted and every request on the gateway's main port fails with a
server misconfiguration error, the exempt routes above included (`/health`, the Web UI's files,
the App API, and webhooks that share the port).

---

## Multi-Agent Setup

GoClaw allows you to define multiple "Agents," each with its own isolated identity, model, and workspace.

### Defining Agents
In `config.yaml`, agents are listed under `agents.list`.

```yaml
agents:
  default: "main"  # Fallback agent ID
  list:
    - id: "main"
      name: "Personal Assistant"
      model: "gemini-2.0-flash"
    - id: "coding"
      name: "Code Expert"
      model: "claude-sonnet-4-5-20250929"
      skills:
        allow: ["git", "coding"]
      tools:
        shell:
          allow: ["*"]
```

### Routing Bindings
Bindings map incoming messages from different channels to specific agents. The most specific
match wins: a `peer`, then a thread's parent peer, then `tenant_id` with `platform_roles`, then
`platform_roles` alone, then `tenant_id`, then `account_id`, then the channel alone. A message no
binding matches goes to `agents.default`.

```yaml
bindings:
  - agent_id: "coding"
    match:
      channel: "discord"
      tenant_id: "123456789012345678" # one Discord server (guild ID)
  - agent_id: "main"
    match:
      channel: "discord" # Fallback for all other Discord messages
```

What `tenant_id` and `account_id` hold depends on the channel. On Discord, `tenant_id` is the
server (guild) ID and `account_id` is the ID of the user who wrote the message. On Microsoft
Teams, `tenant_id` is the team and `account_id` is the bot's app ID; on Telegram `account_id` is
the bot's ID, and on Facebook the Page ID. `peer: {kind: "direct" | "group", id: "…"}` matches
one conversation by its ID. See [Agent Routing](../architecture/routing.md).

---

## MCP Setup (Model Context Protocol)

GoClaw provides native support for MCP, allowing agents to connect to external servers for tools, resources, and prompts.

### Configuring MCP Servers
You can define MCP servers in `config.yaml` using either `stdio` (command-based) or `HTTP/SSE`.

```yaml
mcp:
  servers:
    # Stdio-based server (runs as a subprocess)
    filesystem:
      command: "npx"
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    
    # Remote server via HTTP/SSE
    remote:
      url: "https://mcp.example.com"
      transport: "sse" # auto | streamable | sse
      headers:
        Authorization: "Bearer ${secret:MCP_TOKEN}"
```

### Using MCP via Tools
When an MCP server is configured, agents gain access to the `mcp` tool. They can:
1. `connect` to a server.
2. `list_tools` to see what is available.
3. `call_tool` to execute an MCP-hosted tool.

---

## Session Management

### Session Reset Rules
Clear sessions automatically, every day at an hour or after a time with no
message. `reset` is the rule for every session. `reset_by_type` sets a rule per
channel; its key is a channel name as it appears in a session key, such as
`telegram`, `discord`, `msteams` or `facebook`. `reset_triggers` are the chat
commands that clear a session on demand.

Automatic resets reach the sessions of every agent in `agents.list`, each in
its own session store.

```yaml
session:
  reset:
    mode: daily        # daily | idle
    at_hour: 4         # UTC hour, for daily
  reset_by_type:
    msteams:
      mode: idle
      idle_minutes: 60 # cleared after an hour with no message
  reset_triggers: ["/new", "/reset"]
```

A reset never cuts into a reply. When a session is still answering a message,
a daily reset waits for the reply to be saved and then clears the session. An
idle reset leaves it alone, since a session that is answering is not idle, and
its idle time starts again.

Idle time counts from the last message the running gateway saw: after a
restart, a session's idle time starts with its next message.

---

## Tool Safety & Loop Detection

GoClaw watches each session's tool calls for loops. Every call gets a loop
score from the session's last `history_size` calls: the same tool with the same
arguments again, a polling tool called over and over, two tools alternating
(A, B, A, B), one tool called many times with different arguments, or one call
repeated in between others. The thresholds are compared with that score, not
with a count of repeats.

- **Warning** (`warning_threshold`, default 10): written to the log only.
- **Critical** (`critical_threshold`, default 20): the call runs, and a message
  telling the model to change its approach and answer is added to the result.
- **Circuit breaker** (`global_circuit_breaker_threshold`, default 30): the
  call is not run. The model gets a message to stop calling tools and answer
  with what it has.

```yaml
tools:
  loop_detection:
    enabled: true
    history_size: 100
    warning_threshold: 10
    critical_threshold: 20
    global_circuit_breaker_threshold: 30
    detectors:                 # all on by default
      generic_repeat: true          # same tool, same arguments
      known_poll_no_progress: true  # a polling tool called again and again
      ping_pong: true               # A, B, A, B
      same_tool_flood: true         # one tool, many different arguments
      identical_repeat_ratio: true  # one call repeated between other calls
```

---

## Context Pruning

To keep LLM context windows efficient, GoClaw can prune old or large tool results.

```yaml
context_pruning:
  mode: "cache-ttl" # off | cache-ttl
  soft_trim:
    head_chars: 1000
    tail_chars: 1000
```

---

## Observability

Configure integration with external observability tools like Grafana or Graylog.

```yaml
tools:
  observability:
    grafana:
      prod:
        url: "http://grafana.prod:3000"
        token: "${secret:GRAFANA_PROD_TOKEN}"
```

---

## Gateway Ingress & Webhook Isolation

To protect the internal Web UI and APIs from accidental public exposure via reverse proxies (like Nginx), you can isolate the webhook receiver on a dedicated port. 

When `gateway.ingress.port` is specified, GoClaw creates a completely separate webhook-only HTTP server.

```yaml
gateway:
  bind: "loopback"
  ingress:
    type: "proxy"
    port: "8081"  # Dedicated listener for webhooks (/hook/*, /goclaw/hook/*, /api/webhook)
    bind: "lan"   # the proxy is on another host; the default, loopback, takes only local connections
    allow_ips: ["10.0.0.0/8"]
```

This ensures that even if your proxy indiscriminately forwards traffic to `8081`, the Web UI remains safe and hidden on `server.port` (default `8080`). Health probes (`/health`, `/ready`) will remain accessible on both listeners.

The dedicated listener binds to `gateway.ingress.bind`, which defaults to loopback (`127.0.0.1`): set
`lan` (or an IP) when the proxy or the sender is on another host. With a dedicated port, the webhook
routes (`/hook/*`, `/goclaw/hook/*`) are served only there: a hook URL that points at `server.port`
gets `404`. See [Public Ingress](ingress.md).

### Rate limiting

```yaml
gateway:
  rate_limit:
    enabled: true     # default
    per_minute: 60    # per client IP (default 60)
    trust_proxy: false # take the client IP from X-Forwarded-For
```

The budget covers what can be abused without the gateway token: requests with a missing or wrong token (so guessing the token stays limited), webhooks, and the app API. Requests that carry the gateway token and the Web UI's own static files are not counted, so opening the dashboard — dozens of scripts and API calls — does not use it up. Over-budget requests get `429` with `Retry-After`. Behind a reverse proxy, enable `trust_proxy`, or every client shares the proxy's IP and budget.

---

## Webhook Routes

`webhooks:` lists the routes of the webhook receiver at `/goclaw/hook/<type>[/<key>]`: which agent
an outside system's POST reaches, the credential it must carry, and whether it starts a SOP.
Routes can also be made on the **Webhooks** page or through `POST /api/webhooks`
([Gateway Endpoints](../api-reference/gateway_endpoints.md#webhook-routes-api)); those are stored
in the database. A route for the exact key comes before a route of its type without a key; between
two routes for the same key, the stored one comes first.

```yaml
webhooks:
  # Graylog alert → the incident-triage SOP, once per alert
  - type: graylog
    key: payments-errors
    secret: "${secret:GRAYLOG_HOOK_PAYMENTS}"
    agent: main
    sop_id: incident-triage

  # GitLab events → a chat turn on agent main
  - type: gitlab
    key: myproject
    secret: "${secret:GITLAB_WEBHOOK_MYPROJECT}"
    agent: main
    events: [push, pipeline, merge_request]
```

The first route answers `https://<gateway>/goclaw/hook/graylog/payments-errors`.

| Key | Meaning |
|---|---|
| `type` | First path segment: `/goclaw/hook/<type>`. `gitlab`, `github`, `graylog` and `zabbix` payloads are read specially; any other name is a generic route. A Graylog notification needs `type: graylog`: under any other type, Graylog's test notification would run like an alert. |
| `key` | Second path segment: `/goclaw/hook/<type>/<key>`. A route without a key takes every key of its type that has no route of its own. |
| `secret` | The shared secret. GitLab sends it as `X-Gitlab-Token`; GitHub signs the body with it (`X-Hub-Signature-256`). Every other type, Graylog included, accepts it as an `X-Token` header, a `?token=` query parameter, or an `X-Signature: sha256=<hex>` HMAC of the body. |
| `token` | A second credential, compared only with the `?token=` query parameter. |
| `agent` | The agent ID that gets the event, or `"*"` for every agent. A chat turn runs on that agent's `model`. An agent the gateway is not running (not in `agents.list`, added since the gateway started, or its model failed to start) does not get it: the delivery is answered `409`, or, on a `"*"` route, goes to the other agents. |
| `events` | Event names to take; empty takes every event. Any other event is answered `200 {"status":"filtered"}` and runs nothing. |
| `sop_id` | Run this SOP for the event instead of a chat turn. See [Runs started by a webhook](automation/sop.md#runs-started-by-a-webhook). |
| `batch_mode` | With `sop_id`: `single` (default) runs the SOP once per event, except that events about an object whose run is in progress wait for it and then run once, together ([Runs started by a webhook](automation/sop.md#runs-started-by-a-webhook)); `bulk` gathers the route's events and runs it once for them. |
| `batch_window` | With `batch_mode: bulk`: how many seconds the route gathers events (default 300). A run takes at most 500 events, oldest first; the rest wait for the next run. |

A route with neither `secret` nor `token` refuses every delivery with `401`. Keep the secret in the
vault (`goclaw secret set GRAYLOG_HOOK_PAYMENTS <value>`) and write a `${secret:…}` reference.

The receiver reads `webhooks:` for each delivery, so a change saved to `config.yaml` applies to the
next delivery without a restart.

A `config.yaml` route is listed on the Webhooks page, but it cannot be paused, edited or deleted
there, and it records no hit count or last error: look for `[webhook]` lines in the gateway log
instead. A route made on the page, or through `/api/webhooks`, shows both.

An admin can also ask the agent in chat, for example "what did the build-events webhook receive
today?". Its `webhooks` tool lists the routes with their hit count, last hit and last error, and
shows a route's recent deliveries: when each came, its event, and the summary of the payload the
receiver handed to the agent or SOP. It never shows a route's secret or token, and it is read-only.
Only deliveries that reached an agent or a SOP are there; one refused or filtered out shows only in
the route's hit count and last error. Anyone else who asks is told that this needs an administrator.

The address the sender calls is the gateway's public address (see [Public Ingress](ingress.md)).
Step-by-step guides: [GitLab Webhook Integration](automation/gitlab-webhook.md) and
[Graylog Alerts That Run a SOP](automation/graylog-alerts.md).

---

## State Database Configuration

GoClaw persists sessions, job queues, SOP definitions, scheduled cron states, agent metrics, and auto-learning logs through GORM. The `database:` block configures the backend driver:

```yaml
database:
  driver: "sqlite" # "sqlite" (default) | "postgres" | "mysql" | "clickhouse"
  # path: "/var/lib/goclaw/state.sqlite" # SQLite only; leave unset for ~/.goclaw/gc_state.sqlite
```

Leave `path` unset unless you mean to move the database. A relative path is taken inside the state
directory, so `path: "sessions.db"` opens a new, empty `~/.goclaw/sessions.db` instead of the
existing `gc_state.sqlite`.

### Supported Drivers & DSN Examples

| Driver | Description | Connection |
|---|---|---|
| `sqlite` | Embedded SQLite (WAL mode, zero setup) | `path` (default `~/.goclaw/gc_state.sqlite`) |
| `clickhouse` | High-throughput analytics & distributed storage | `dsn: "clickhouse://user:password@localhost:9000/goclaw"` |
| `postgres` | Enterprise relational storage | `dsn: "host=localhost user=goclaw password=secret dbname=goclaw sslmode=disable"` |
| `mysql` | MySQL / MariaDB relational storage | `dsn: "goclaw:secret@tcp(localhost:3306)/goclaw?charset=utf8mb4&parseTime=True&loc=Local"` |

`dsn` is read only by the `postgres`, `mysql` and `clickhouse` drivers, which require it; `sqlite` reads only `path`.
