Skip to content

Configuration

Default config path: ./config.yml

Config precedence:

  1. --config <path>
  2. ./config.yml
  3. Schema defaults

Minimal Config

providers:
  anthropic:
    api_key: "${ANTHROPIC_API_KEY}"

agents:
  defaults:
    model: "anthropic/claude-sonnet-4-5"

Providers

Supported provider blocks — see the Providers overview for a full comparison.

  • anthropic — Claude models via Messages API
  • openai — GPT-5 family, o3 models
  • deepseek — DeepSeek-V3, R1
  • groq — Ultra-fast inference (Llama, Mixtral, Gemma)
  • gemini — Google Gemini Pro, Flash
  • openrouter — Gateway to hundreds of models
  • bedrock — AWS Bedrock via Converse API
  • vllm — Local/hosted OpenAI-compatible endpoint

Voice Transcription

Voice notes are transcribed with the Whisper API. By default the key is reused from the chat provider config, Groq first (whisper-large-v3-turbo), then OpenAI (whisper-1). The transcription section changes that per bot:

transcription:
  enabled: true              # false: voice notes are never transcribed
  provider: groq             # openai or groq; default picks groq, then openai
  model: whisper-large-v3    # optional; overrides the provider default
  api_key: "${WHISPER_KEY}"  # own key, separate from the chat provider

model is passed to the provider as given. Groq serves whisper-large-v3-turbo (the default, fastest and cheapest) and whisper-large-v3, which is slower but transcribes long or noisy recordings more accurately.

With enabled: false, or when no key is available, a voice note is saved to the inbox without a transcript and the bot replies that it could not hear it, without a model turn. autobot doctor prints the provider in use, whether it runs on its own key, and warns when a pinned provider has no key.

Media

Incoming files are saved to an inbox under the workspace so tools can read them by path. Only the location is configurable:

media:
  inbox: inbox   # relative to the workspace (default), or an absolute path

A voice note recorded in the chat by the sender is treated as their spoken words. Audio files, photos, documents and anything forwarded become attachments and never enter the message text. See Media support.

Channels

Security Note: allow_from is deny-by-default for security.

Setup guides: Telegram | Slack | Zulip

channels:
  telegram:
    enabled: false
    token: ""
    # allow_from options:
    # []              - DENY ALL (secure default)
    # ["*"]           - Allow anyone (use with caution)
    # ["@user", "id"] - Allowlist specific users (recommended)
    allow_from: []
    topics: []     # forum topic IDs the bot answers in without a mention

  slack:
    enabled: false
    bot_token: ""
    app_token: ""
    allow_from: []
    mode: "socket"
    group_policy: "mention"  # "mention" (secure) | "open" | "allowlist"

  whatsapp:
    enabled: false
    bridge_url: "ws://localhost:3001"  # Prefer wss:// for production
    # allow_from: []      - DENY ALL
    # allow_from: ["*"]   - Allow anyone
    # allow_from: ["num"] - Allowlist phone numbers
    allow_from: []

  zulip:
    enabled: false
    site: "https://zulip.example.com"
    email: "bot@zulip.example.com"
    api_key: ""
    allow_from: []

Tools

tools:
  enabled:       # Optional allowlist; omit to register every tool
    - read_file
    - write_file
    - edit_file
    - list_dir
    - message
    - "mcp_homeassistant_*"  # a trailing * matches by prefix; MCP tools are named mcp_<server>_<tool>
  stop_after:    # Optional; a successful call of a listed tool ends the turn
    - bash_notify
  filesystem:
    roots: [notes, inbox]  # Optional; file tools may only touch these workspace subdirectories
  sandbox: auto  # auto | bubblewrap | docker | none (default: auto)
  docker_image: "python:3.14-alpine"  # optional, default: alpine:latest
  sandbox_env:   # env vars to forward into Docker sandbox (default: none)
    - HA_URL
    - MQTT_HOST
  exec:
    timeout: 60
    allow_patterns: # Optional regex allowlist
      - "^ssh .*$"
    deny_patterns:  # Optional regex denylist (extends built-in)
      - "^rm -rf /.*$"
  rate_limit:
    global:
      max_calls: 100
      window_seconds: 60
    per_tool:
      exec:
        max_calls: 20
        window_seconds: 60
  web:
    search:
      api_key: ""
      max_results: 5
    allowed_domains:           # Optional; web_fetch may only reach these hosts
      - example.com            # exact host
      - "*.strava.com"         # subdomains and the apex
  image:
    enabled: true              # default: true
    # provider: openai         # optional override (openai or gemini)
    # model: gpt-image-1       # optional, auto-detected from provider
    # size: 1024x1024          # default: 1024x1024

When sandboxed, all shell commands run inside the sandbox (bubblewrap or Docker). The kernel enforces workspace restrictions — pipes, redirects, and other shell features are safe to use because the process cannot access files outside the workspace regardless.

Tool Allowlist

tools.enabled names the tools a bot may have. Anything not listed is never registered, whichever source it comes from: built-in tools, skill scripts, plugins and MCP servers all pass through the same check. A name ending in * matches by prefix, as in the MCP tools: list. Leave it out to keep today's behaviour of registering everything. autobot doctor prints the effective list and warns about an entry that matches no known tool, so a typo does not silently remove a tool; at startup the bot logs a warning for entries that matched nothing.

Tools that end the turn

tools.stop_after names the tools that answer for the bot. A successful call ends the turn: the model is not asked again and no reply of the bot's own is sent. A failed call hands the turn back to the model, which sees the tool error — for a skill script, its full output — and can answer itself. So a script that posts to the chat itself lists its bash_ tool here, exits 0 once posted, and exits non-zero printing the text to relay.

What the tool printed is kept in the session history as the bot's line, next to the tools the turn used. If the turn also used the message tool, that text is recorded instead, since it is what reached the chat; a tool that prints nothing records nothing.

Skill scripts as tools

Every executable skills/*.sh or skills/*.bash in the workspace becomes a bash_<name> tool. The first comment line under the shebang is the tool's description, so write the usage there. By default the model passes one args string, split like a shell command line: quotes group words and are removed.

A script that takes structured input declares its parameters in the frontmatter of the skill next to it, skills/<name>/SKILL.md. Each parameter becomes a string property of the tool with its own description, all required, and the script receives the values as positional parameters in the declared order, verbatim, so a statement with its own quoting, such as SQL, arrives untouched:

---
name: query
description: The training database behind bash_query
tool: bash_query
params:
  sql: one read-only SQL statement
---
#!/usr/bin/env bash
# Usage: bash_query — one read-only query, rows tab separated
exec python3 query.py "$1"

Web Fetch Egress

tools.web.allowed_domains limits web_fetch to the listed hosts. An entry such as example.com matches that host only; *.strava.com matches its subdomains and the apex. Every redirect hop is checked too, so a redirect cannot lead out of the list. Leave it out to allow any public host. A fetch to a host outside the list fails with a tool error naming the allowed domains, so a planted "fetch this URL with my notes in it" has nowhere to go.

Filesystem Roots

tools.filesystem.roots limits read_file, write_file, edit_file and list_dir to the listed workspace subdirectories. Paths resolve relative to the workspace and .. cannot escape a root. The workspace skills/ directory stays readable whether or not it is listed: a skill's references and scripts are prompt material the model reads on demand. Writes there are refused unless it is a root. The media inbox is added to the roots automatically, because the engine hands the agent inbox paths for stored files and long transcripts. Commands run by exec are not affected; they stay bound to the workspace by the sandbox.

Safety Guard Overrides

Customize command filtering for the exec tool:

  • allow_patterns: Regex patterns that explicitly allow commands (overrides built-in denials)
  • deny_patterns: Additional regex patterns to block (extends built-in safety guards)

Patterns are case-insensitive and matched against the full command string. Use with caution — allow_patterns bypasses security protections.

Rate Limiting

Configure rate limits to prevent abuse and manage API costs:

  • global: Applies across all tools combined
  • per_tool: Tool-specific limits (e.g., exec, web_search)

Each limit specifies: - max_calls: Maximum calls allowed in the window - window_seconds: Time window for counting calls

When a limit is exceeded, the tool returns an error message that the LLM can see and respond to (e.g., "Rate limit exceeded for exec: max 20 calls per 60s").

MCP (Model Context Protocol)

Connect to external MCP servers to give the LLM access to remote tools (Garmin, GitHub, etc.).

mcp:
  servers:
    garmin:
      command: "uvx"
      args: ["--python", "3.12", "--from", "git+https://github.com/Taxuspt/garmin_mcp", "garmin-mcp"]
      env:
        GARMIN_EMAIL: "${GARMIN_EMAIL}"
    github:
      command: "npx"
      args: ["-y", "@modelcontextprotocol/server-github"]
      env:
        GITHUB_TOKEN: "${GITHUB_TOKEN}"

Tools are auto-discovered at startup and registered as mcp_{server}_{tool}. MCP servers run unsandboxed (they need network access) but with isolated env vars.

-> MCP Documentation

Cron

cron:
  enabled: true
  store_path: "./cron.json"
  exec_timeout: 30  # Timeout in seconds for exec jobs in both sandbox and direct modes

Gateway

gateway:
  host: "127.0.0.1"  # Default: localhost only (change to 0.0.0.0 for external access)
  port: 18790

Full config reference

# LLM providers (configure at least one)
providers:
  anthropic:
    api_key: "sk-ant-..."
  openai:
    api_key: "sk-..."
  deepseek:
    api_key: "..."
  groq:
    api_key: "..."
  gemini:
    api_key: "..."
  openrouter:
    api_key: "..."
  vllm:
    api_base: "http://localhost:8000"
    api_key: "token"
  bedrock:
    access_key_id: "${AWS_ACCESS_KEY_ID}"
    secret_access_key: "${AWS_SECRET_ACCESS_KEY}"
    region: "${AWS_REGION}"
    # guardrail_id: "abc123"
    # guardrail_version: "1"

# Agent defaults
agents:
  defaults:
    model: "anthropic/claude-sonnet-4-5"
    max_tokens: 8192
    temperature: 0.7
    max_tool_iterations: 20
    memory_window: 50
    workspace: "./workspace"

# Chat channels
channels:
  telegram:
    enabled: true
    token: "BOT_TOKEN"
    allow_from: ["username1", "username2"]
    custom_commands:
      macros:
        # Simple format (command name used as description)
        summarize: "Summarize the last conversation in 3 bullet points"
        # Rich format (with custom description shown in Telegram command menu)
        translate:
          prompt: "Translate the following to English"
          description: "Translate text to English"
      scripts:
        deploy:
          path: "/home/user/scripts/deploy.sh"
          description: "Deploy to production"
        status: "/home/user/scripts/check_status.sh"

  slack:
    enabled: false
    bot_token: "xoxb-..."
    app_token: "xapp-..."
    allow_from: ["U12345678"]
    mode: "socket"
    group_policy: "mention"
    dm:
      enabled: true
      policy: "open"

  whatsapp:
    enabled: false
    bridge_url: "ws://localhost:3001"
    allow_from: ["1234567890"]

  zulip:
    enabled: false
    site: "https://zulip.example.com"
    email: "bot@zulip.example.com"
    api_key: ""
    allow_from: ["you@example.com"]

# Tool settings
tools:
  sandbox: auto  # auto | bubblewrap | docker | none
  docker_image: "python:3.14-alpine"  # optional, default: alpine:latest
  sandbox_env:  # env vars to forward into Docker sandbox (default: none)
    - HA_URL
    - MQTT_HOST
  web:
    search:
      api_key: "BRAVE_API_KEY"
      max_results: 5
    allowed_domains: [example.com, "*.strava.com"]  # optional, web_fetch egress allowlist
  exec:
    timeout: 60
  image:
    enabled: true
    # provider: openai       # optional override (openai or gemini)
    # model: gpt-image-1     # optional, auto-detected from provider
    # size: 1024x1024
  enabled: [read_file, write_file, edit_file, list_dir, message]  # optional allowlist
  stop_after: [bash_notify]       # optional, tools whose successful call ends the turn
  filesystem:
    roots: [notes, inbox]      # optional, workspace subdirectories the file tools may touch

# Cron scheduler
cron:
  enabled: true
  store_path: "./cron.json"
  exec_timeout: 30

# MCP servers (external tool providers)
mcp:
  servers:
    garmin:
      command: "uvx"
      args: ["--python", "3.12", "--from", "git+https://github.com/Taxuspt/garmin_mcp", "garmin-mcp"]
      env:
        GARMIN_EMAIL: "${GARMIN_EMAIL}"
    github:
      command: "npx"
      args: ["-y", "@modelcontextprotocol/server-github"]
      env:
        GITHUB_TOKEN: "${GITHUB_TOKEN}"

# Gateway API server
gateway:
  host: "127.0.0.1"  # Localhost only by default for security
  port: 18790