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.
GoClaw can be configured using multiple methods:
config.yaml in the state directory (~/.goclaw/config.yaml), else config.yaml in the working directory.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.goclaw setup. It opens by itself the first time goclaw starts with no config.yaml.The wizard offers QuickStart and Advanced. QuickStart asks only what GoClaw cannot guess:
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.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.
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:
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.
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:
# 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
ai: says which model provider answers and holds each provider's credentials. Only the block of a
provider you use needs a key.
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.model in agents.list (see 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. 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.ai.token_budget, and
each SOP run stops at 500,000 tokens unless sop.max_run_tokens says
otherwise: see Token Budgeting.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.
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:
X-Goclaw-Token: <token> header,Authorization: Bearer <token> header,?token=<token> query parameter (WebSocket and SSE clients),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).
GoClaw allows you to define multiple "Agents," each with its own isolated identity, model, and workspace.
In config.yaml, agents are listed under agents.list.
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: ["*"]
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.
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.
GoClaw provides native support for MCP, allowing agents to connect to external servers for tools, resources, and prompts.
You can define MCP servers in config.yaml using either stdio (command-based) or HTTP/SSE.
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}"
When an MCP server is configured, agents gain access to the mcp tool. They can:
connect to a server.list_tools to see what is available.call_tool to execute an MCP-hosted tool.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.
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.
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_threshold, default 10): written to the log only.critical_threshold, default 20): the call runs, and a message
telling the model to change its approach and answer is added to the result.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.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
To keep LLM context windows efficient, GoClaw can prune old or large tool results.
context_pruning:
mode: "cache-ttl" # off | cache-ttl
soft_trim:
head_chars: 1000
tail_chars: 1000
Configure integration with external observability tools like Grafana or Graylog.
tools:
observability:
grafana:
prod:
url: "http://grafana.prod:3000"
token: "${secret:GRAFANA_PROD_TOKEN}"
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.
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.
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.
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); 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.
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. |
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); 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). Step-by-step guides: GitLab Webhook Integration and Graylog Alerts That Run a SOP.
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:
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.
| 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.