deepseek-harness / cli-config.yaml.example
Deploy
Deploy Hermes Agent v0.20.4 (Docker SDK)
a37e1b9
Raw History Blame Contribute Delete
100 kB
# Hermes Agent CLI Configuration
# Copy settings from this example into ~/.hermes/config.yaml, or use
# `hermes config set <section.key> <value>` to update the active profile.
# This file configures CLI behavior; only documented secret environment
# variables in .env take precedence over their corresponding settings.
# =============================================================================
# Database Configuration
# =============================================================================
# WAL is the normal default. Hermes automatically falls back to DELETE when
# SQLite reports that WAL is incompatible with the filesystem. Set this to
# "delete" explicitly for deployments whose backing filesystem is not WAL
# crash-safe, such as Linux containers bind-mounted through macOS virtiofs,
# NFS, or SMB. Hermes will not live-downgrade a database already open in WAL.
database:
journal_mode: "wal" # Supported values: "wal", "delete"
# Optional WAL sizing pragmas (integers). Unset = SQLite defaults.
# wal_autocheckpoint: 1000 # pages between automatic checkpoints
# journal_size_limit: 67108864 # cap the WAL/journal file size in bytes
# =============================================================================
# Runtime Limits
# =============================================================================
# Long-running Hermes server processes raise their RLIMIT_NOFILE soft limit to
# this value when the operating system permits it. The value is clamped to the
# hard limit and never lowers an already higher soft limit. Set to 0, false, or
# null to disable the adjustment. Default: 4096.
runtime:
nofile_soft_limit: 4096
# =============================================================================
# Model Configuration
# =============================================================================
model:
# Default model to use (can be overridden with --model flag)
# Both "default" and "model" work as the key name here.
default: "anthropic/claude-opus-4.6"
# Inference provider selection:
# "auto" - Auto-detect from credentials (default)
# "openrouter" - OpenRouter (requires: OPENROUTER_API_KEY or OPENAI_API_KEY)
# "nous" - Nous Portal OAuth (requires: hermes auth add nous)
# "nous-api" - Nous Portal API key (requires: NOUS_API_KEY)
# "anthropic" - Direct Anthropic API (requires: ANTHROPIC_API_KEY)
# "openai-codex" - OpenAI Codex (requires: hermes auth)
# "copilot" - GitHub Copilot / GitHub Models (requires: GITHUB_TOKEN)
# "gemini" - Use Google AI Studio direct (requires: GOOGLE_API_KEY or GEMINI_API_KEY)
# "zai" - Use z.ai / ZhipuAI GLM models (requires: GLM_API_KEY)
# "kimi-coding" - Kimi / Moonshot AI (requires: KIMI_API_KEY)
# "minimax" - MiniMax global (requires: MINIMAX_API_KEY)
# "minimax-cn" - MiniMax China (requires: MINIMAX_CN_API_KEY)
# "huggingface" - Hugging Face Inference (requires: HF_TOKEN)
# "nvidia" - NVIDIA NIM / build.nvidia.com (requires: NVIDIA_API_KEY)
# "xiaomi" - Xiaomi MiMo (requires: XIAOMI_API_KEY)
# "arcee" - Arcee AI Trinity models (requires: ARCEEAI_API_KEY)
# "ollama-cloud" - Ollama Cloud (requires: OLLAMA_API_KEY β€” https://ollama.com/settings)
# "deepinfra" - DeepInfra (requires: DEEPINFRA_API_KEY)
# "kilocode" - KiloCode gateway (requires: KILOCODE_API_KEY)
# "ai-gateway" - Vercel AI Gateway (requires: AI_GATEWAY_API_KEY)
# "azure-foundry" - Microsoft Foundry / Azure OpenAI (API key or Entra ID)
# "lmstudio" - LM Studio local server (optional: LM_API_KEY, defaults to http://127.0.0.1:1234/v1)
#
# Local servers (LM Studio, Ollama, vLLM, llama.cpp):
# "custom" - Any other OpenAI-compatible endpoint. Set base_url below.
# Aliases: "ollama", "vllm", "llamacpp" all map to "custom".
# LM Studio is first-class and uses provider: "lmstudio".
# It works with both no-auth and auth-enabled server modes.
#
# Can also be overridden for a single invocation with the --provider flag.
provider: "auto"
# API configuration (falls back to OPENROUTER_API_KEY env var)
# api_key: "your-key-here" # Uncomment to set here instead of .env
base_url: "https://openrouter.ai/api/v1"
# Azure Foundry keyless auth example:
# provider: "azure-foundry"
# base_url: "https://<resource>.openai.azure.com/openai/v1"
# auth_mode: "entra_id" # DefaultAzureCredential: az login, managed identity, workload identity, etc.
# default: "gpt-4o" # Deployment/model name
# entra:
# scope: "https://ai.azure.com/.default" # Optional; this is the default.
# ── Token limits β€” two settings, easy to confuse ──────────────────────────
#
# context_length: TOTAL context window (input + output tokens combined).
# Controls when Hermes compresses history and validates requests.
# Leave unset β€” Hermes auto-detects the correct value from the provider.
# Set manually only when auto-detection is wrong (e.g. a local server with
# a custom num_ctx, or a proxy that doesn't expose /v1/models).
#
# context_length: 131072
#
# max_tokens: OUTPUT cap β€” maximum tokens the model may generate per response.
# Unrelated to how long your conversation history can be.
# The OpenAI-standard name "max_tokens" is a misnomer; Anthropic's native
# API has since renamed it "max_output_tokens" for clarity.
# Leave unset to use the model's native output ceiling (recommended).
# Set only if you want to deliberately limit individual response length.
#
# max_tokens: 8192
# ── Custom request headers (optional) ─────────────────────────────────────
#
# default_headers: extra HTTP headers sent on every request to an
# OpenAI-compatible endpoint. User values take precedence over the
# provider/SDK defaults, so this is the supported way to override the
# OpenAI Python SDK's identifying headers (User-Agent: OpenAI/Python ...,
# X-Stainless-*) when a custom provider sits behind a gateway/WAF that
# rejects them β€” e.g. an upstream that returns "502 Upstream access
# forbidden" for the SDK default User-Agent but accepts a plain one.
# Applies on the OpenAI wire only (not native Anthropic / Bedrock).
#
# default_headers:
# User-Agent: "curl/8.7.1"
#
# extra_headers: accepted as an alias of default_headers (merged, with
# extra_headers winning when both are set) β€” matches the per-provider
# extra_headers key below.
#
# Per-provider variant: named providers / custom_providers entries accept an
# extra_headers dict scoped to that endpoint only β€” for reverse proxies,
# gateways, or custom auth (e.g. Cloudflare Access service tokens).
# Merged onto SDK/provider defaults with the entry's values winning.
# Header values are treated as secrets and are never logged.
#
# providers:
# my-proxy:
# base_url: "https://llm.internal.example.com/v1"
# key_env: "MY_PROXY_API_KEY"
# extra_headers:
# CF-Access-Client-Id: "xxxx.access"
# CF-Access-Client-Secret: "${CF_ACCESS_SECRET}"
# X-Client-Name: "hermes-agent"
# providers:
# meta:
# base_url: https://api.meta.ai/v1
# api_key: ${MODEL_API_KEY}
# # api_mode auto-detected as codex_responses for api.meta.ai; no need to set
# # (the bundled meta-ai provider covers this β€” a named custom provider is
# # only needed for a non-default Meta-compatible endpoint)
# Command-minted credentials (optional): key_cmd
# ------------------------------------------------------------------
# Enterprise gateways often issue SHORT-LIVED bearers (SSO/OIDC brokers, cloud
# IAM, internal auth proxies) rather than static API keys, so a value copied
# into .env via `key_env` is stale within the hour and every later request 401s.
# `key_cmd` names a command that PRINTS a token instead: Hermes runs it per
# request (cached until shortly before expiry), so long sessions keep working
# with no restart.
#
# Contract: print ONLY the token on stdout, either bare or as JSON with an
# "access_token" field ("expires_in" is honoured). Same shape as OAuth 2.0
# token endpoints, Claude Code's `apiKeyHelper`, `gcloud auth
# print-access-token`, and `aws ecr get-login-password`.
#
# Precedence: an explicit --api-key still wins; otherwise key_cmd is preferred
# over inline api_key / key_env on that entry.
#
# Applies to the main agent turn and to auxiliary tasks (title generation,
# context compression, vision, embedding) alike.
#
# Not to be confused with `secrets.command`, which is a different mechanism:
# that one runs a helper ONCE at startup to populate env vars for many secrets
# at the process level. Use it for a vault or keychain helper that hands back a
# KEY=VALUE blob. Use `key_cmd` when ONE provider needs a credential refreshed
# DURING a session, because a startup-time env var cannot be re-minted after it
# expires.
#
# providers:
# my-gateway:
# base_url: "https://gateway.internal.example.com/v1"
# api_mode: chat_completions
# key_cmd: "my-auth-cli print-token --profile prod"
#
# Worked example β€” an AI gateway that routes by model family, so one entry per
# wire format shares the same credential helper:
#
# providers:
# dbx: # OpenAI-compatible (MLflow) route
# base_url: "https://<workspace>.cloud.databricks.com/ai-gateway/mlflow/v1"
# api_mode: chat_completions
# model: databricks-claude-sonnet-4-6
# key_cmd: "databricks auth token -p MY-PROFILE"
# dbx-gpt: # OpenAI Responses route
# base_url: "https://<workspace>.cloud.databricks.com/ai-gateway/openai/v1"
# api_mode: codex_responses
# model: databricks-gpt-5-5
# key_cmd: "databricks auth token -p MY-PROFILE"
# dbx-claude: # Anthropic Messages route
# base_url: "https://<workspace>.cloud.databricks.com/ai-gateway/anthropic"
# api_mode: anthropic_messages
# model: databricks-claude-fable-5
# key_cmd: "databricks auth token -p MY-PROFILE"
# Named provider overrides (optional)
# Use this for per-provider request timeouts, non-stream stale timeouts,
# and per-model exceptions.
# Applies to the primary turn client on every api_mode (OpenAI-wire, native
# Anthropic, and Anthropic-compatible providers), the fallback chain, and
# client rebuilds during credential rotation. For OpenAI-wire chat
# completions (streaming and non-streaming) the configured value is also
# used as the per-request ``timeout=`` kwarg so it wins over the legacy
# HERMES_API_TIMEOUT env var (which still applies when no config is set).
# ``stale_timeout_seconds`` controls the non-streaming stale-call detector and
# wins over the legacy HERMES_API_CALL_STALE_TIMEOUT env var. Leaving these
# unset keeps the legacy defaults (HERMES_API_TIMEOUT=1800s,
# HERMES_API_CALL_STALE_TIMEOUT=90s, native Anthropic 900s). The
# implicit non-stream stale detector is auto-disabled for local endpoints
# and can scale upward for very large contexts.
#
# Not currently wired for AWS Bedrock (bedrock_converse + AnthropicBedrock
# SDK paths) β€” those use boto3 with its own timeout configuration.
#
# providers:
# ollama-local:
# request_timeout_seconds: 300 # Longer timeout for local cold-starts
# stale_timeout_seconds: 900 # Explicitly re-enable stale detection on local endpoints
# anthropic:
# request_timeout_seconds: 30 # Fast-fail cloud requests
# models:
# claude-opus-4.6:
# timeout_seconds: 600 # Longer timeout for extended-thinking Opus calls
# openai-codex:
# models:
# gpt-5.4:
# stale_timeout_seconds: 1800 # Longer non-stream stale timeout for slow large-context turns
# =============================================================================
# Unified Timeouts (operation deadlines)
# =============================================================================
# One place to override Hermes's internal operation deadlines (seconds).
# Keys are dotted paths resolved by agent/deadline.py:resolve_timeout().
# Precedence: this section > legacy HERMES_* env var (back-compat) > built-in
# default. 0 or a negative value disables the bound (unbounded); very large
# values are clamped to a platform-safe maximum automatically.
#
# Currently resolved keys (more paths migrate here over time β€” see issue #85125):
#
# timeouts:
# tools:
# concurrent_batch: 420 # Deadline for a parallel tool-call batch
# # (legacy env: HERMES_CONCURRENT_TOOL_TIMEOUT_S)
# sequential_call: 420 # Deadline for one sequentially-executed tool call.
# # Defaults to concurrent_batch's value so the two
# # executor paths stay in sync; human waits
# # (approval prompts, clarify) never count against it.
# =============================================================================
# OpenRouter Provider Routing (only applies when using OpenRouter)
# =============================================================================
# Control how requests are routed across providers on OpenRouter.
# See: https://openrouter.ai/docs/guides/routing/provider-selection
#
# provider_routing:
# # Sort strategy: "price" (default), "throughput", or "latency"
# # Append :nitro to model name for a shortcut to throughput sorting.
# sort: "throughput"
#
# # Only allow these providers (provider slugs from OpenRouter)
# # only: ["anthropic", "google"]
#
# # Skip these providers entirely
# # ignore: ["deepinfra", "fireworks"]
#
# # Try providers in this order (overrides default load balancing)
# # order: ["anthropic", "google", "together"]
#
# # Require providers to support all parameters in your request
# # require_parameters: true
#
# # Data policy: "allow" (default) or "deny" to exclude providers that may store data
# # data_collection: "deny"
# =============================================================================
# OpenRouter Response Caching (only applies when using OpenRouter)
# =============================================================================
# Cache identical API responses at the OpenRouter edge for free instant replays.
# When enabled, identical requests (same model, messages, parameters) return
# cached responses with zero billing. Separate from Anthropic prompt caching.
# See: https://openrouter.ai/docs/guides/features/response-caching
#
# openrouter:
# response_cache: true # Enable response caching (default: true)
# response_cache_ttl: 300 # Cache TTL in seconds, 1-86400 (default: 300)
# =============================================================================
# Git Worktree Isolation
# =============================================================================
# When enabled, each CLI session creates an isolated git worktree so multiple
# agents can work on the same repo concurrently without file collisions.
# Equivalent to always passing --worktree / -w on the command line.
#
# worktree: true # Always create a worktree when in a git repo
# worktree: false # Default β€” only create when -w flag is passed
#
# By default a new worktree branches from the freshly-fetched remote tip
# (the current branch's upstream, else the remote's default branch) so it
# starts current with the project instead of from the local clone's
# (possibly stale) HEAD. Set worktree_sync: false to branch from local HEAD
# instead β€” useful when offline or when you deliberately want the clone's
# exact current state as the base.
#
# worktree_sync: true # Default β€” branch from the fetched remote tip
# worktree_sync: false # Branch from local HEAD (offline / pinned base)
# =============================================================================
# Kanban Review Dispatch
# =============================================================================
# First-class review tasks are dispatched automatically by default. The worker
# is spawned with the bundled sdlc-review skill and can approve, request changes
# back to the original implementer, or escalate a genuine external blocker.
# Disable this only when every review is performed manually from the dashboard.
kanban:
review_dispatch: true
# =============================================================================
# Terminal Tool Configuration
# =============================================================================
# Choose ONE of the following terminal configurations by uncommenting it.
# The terminal tool executes commands in the specified environment.
# -----------------------------------------------------------------------------
# OPTION 1: Local execution (default)
# Commands run directly on your machine in the current directory
# -----------------------------------------------------------------------------
# Working directory behavior:
# - CLI (`hermes` command): Uses "." (current directory where you run hermes)
# - Gateway/messaging/cron: Uses terminal.cwd here; legacy .env cwd values are deprecated
terminal:
backend: "local"
cwd: "." # For local backend: "." = current directory. Ignored for remote backends unless a backend documents otherwise.
# Desktop xterm font. Install the font locally; Nerd Fonts render Powerlevel10k glyphs.
# font_family: "MesloLGS NF" # Also accepts a CSS stack; blank uses bundled JetBrains Mono.
timeout: 180
# HOME policy for tool subprocesses:
# auto - default: host uses your real HOME; containers use HERMES_HOME/home
# real - force your real OS-user HOME
# profile - force HERMES_HOME/home for strict per-profile CLI config isolation
home_mode: "auto"
docker_mount_cwd_to_workspace: false # SECURITY: off by default. Opt in to mount the launch cwd into Docker /workspace.
lifetime_seconds: 300
# sudo_password: "hunter2" # Optional: pipe a sudo password via sudo -S. SECURITY WARNING: plaintext.
# sudo_password: "" # Explicit empty password: try empty and never open the interactive sudo prompt.
# -----------------------------------------------------------------------------
# OPTION 2: SSH remote execution
# Commands run on a remote server - agent code stays local (sandboxed)
# Great for: keeping agent isolated from its own code, using powerful remote hardware
# -----------------------------------------------------------------------------
# terminal:
# backend: "ssh"
# cwd: "/home/myuser/project" # Path on the REMOTE server
# timeout: 180
# lifetime_seconds: 300
# ssh_host: "my-server.example.com"
# ssh_user: "myuser"
# ssh_port: 22
# ssh_key: "~/.ssh/id_rsa" # Optional - uses ssh-agent if not specified
# -----------------------------------------------------------------------------
# OPTION 3: Docker container
# Commands run in an isolated Docker container
# Great for: reproducible environments, testing, isolation
# -----------------------------------------------------------------------------
# terminal:
# backend: "docker"
# cwd: "/workspace" # Path INSIDE the container (default: /)
# timeout: 180
# lifetime_seconds: 300
# docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
# docker_mount_cwd_to_workspace: true # Explicit opt-in: mount your launch cwd into /workspace
# # Optional: run the container as your host user's uid:gid so files written
# # into bind-mounted dirs are owned by you, not root. Drops SETUID/SETGID
# # caps too since no gosu privilege drop is needed. Leave off if your
# # chosen docker_image expects to start as root.
# docker_run_as_host_user: true
# # Optional: explicitly forward selected env vars into Docker.
# # These values come from your current shell first, then ~/.hermes/.env.
# # Warning: anything forwarded here is visible to commands run in the container.
# docker_forward_env:
# - "GITHUB_TOKEN"
# - "NPM_TOKEN"
# # Optional: extra flags passed verbatim to docker run (appended after security defaults).
# # Useful for adding capabilities (e.g. apt installs needing SETUID) or custom options.
# # Example: add a Linux capability not included by default
# # docker_extra_args:
# # - "--cap-add"
# # - "SETUID"
# -----------------------------------------------------------------------------
# OPTION 4: Singularity/Apptainer container
# Commands run in a Singularity container (common in HPC environments)
# Great for: HPC clusters, shared compute environments
# -----------------------------------------------------------------------------
# terminal:
# backend: "singularity"
# cwd: "/workspace" # Path INSIDE the container (default: /root)
# timeout: 180
# lifetime_seconds: 300
# singularity_image: "docker://nikolaik/python-nodejs:python3.11-nodejs20"
# -----------------------------------------------------------------------------
# OPTION 5: Modal cloud execution
# Commands run on Modal's cloud infrastructure
# Great for: GPU access, scalable compute, serverless execution
# -----------------------------------------------------------------------------
# terminal:
# backend: "modal"
# cwd: "/workspace" # Path INSIDE the sandbox (default: /root)
# timeout: 180
# lifetime_seconds: 300
# modal_image: "nikolaik/python-nodejs:python3.11-nodejs20"
# -----------------------------------------------------------------------------
# OPTION 6: Daytona cloud execution
# Commands run in Daytona cloud sandboxes
# Great for: Cloud dev environments, persistent workspaces, team collaboration
# Requires: pip install daytona, DAYTONA_API_KEY env var
# -----------------------------------------------------------------------------
# terminal:
# backend: "daytona"
# cwd: "~"
# timeout: 180
# lifetime_seconds: 300
# daytona_image: "nikolaik/python-nodejs:python3.11-nodejs20"
# container_disk: 10240 # Daytona max is 10GB per sandbox
#
# --- Container resource limits (docker, singularity, modal, daytona -- ignored for local/ssh) ---
# These settings apply to all container backends. They control the resources
# allocated to the sandbox and whether its filesystem persists across sessions.
container_cpu: 1 # CPU cores
container_memory: 5120 # Memory in MB (5120 = 5GB)
container_disk: 51200 # Disk in MB (51200 = 50GB)
container_persistent: true # Persist filesystem across sessions (false = ephemeral)
# -----------------------------------------------------------------------------
# SUDO SUPPORT (works with ALL backends above)
# -----------------------------------------------------------------------------
# Add sudo_password to any terminal config above to enable sudo commands.
# The password is piped via `sudo -S`. Works with local, ssh, docker, etc.
#
# SECURITY WARNING: Password stored in plaintext!
#
# INTERACTIVE PROMPT: If sudo_password is unset and the CLI is running,
# you'll be prompted to enter your password when sudo is needed:
# - 45-second timeout (auto-skips if no input)
# - Press Enter to skip (command fails gracefully)
# - Password is hidden while typing
# - Password is cached for the session
#
# EMPTY PASSWORDS: Setting sudo_password to an explicit empty string is different
# from leaving it unset. Hermes will try an empty password via `sudo -S` and
# will not open the interactive prompt. This is useful for passwordless sudo,
# Touch ID sudo setups, and environments where prompting is just noise.
#
# ALTERNATIVES:
# - SSH backend: Configure passwordless sudo on the remote server
# - Containers: Run as root inside the container (no sudo needed)
# - Local: Configure /etc/sudoers for specific commands
#
# Example (add to your terminal section):
# sudo_password: "your-password-here"
# =============================================================================
# Security Scanning (tirith)
# =============================================================================
# Optional pre-exec command security scanning via tirith.
# Detects homograph URLs, pipe-to-shell, terminal injection, env manipulation.
# Install: brew install sheeki03/tap/tirith
# Docs: https://github.com/sheeki03/tirith
#
# security:
# tirith_enabled: true # Enable/disable tirith scanning
# tirith_path: "tirith" # Path to tirith binary (supports ~ expansion)
# tirith_timeout: 5 # Scan timeout in seconds
# tirith_fail_open: true # Allow commands if tirith unavailable
# approval:
# transport: builtin # Or an explicitly enabled plugin transport name
# transport_fallback: deny # Set builtin to opt into fallback on transport failure
# =============================================================================
# Browser Tool Configuration
# =============================================================================
browser:
# Inactivity timeout in seconds - browser sessions are automatically closed
# after this period of no activity between agent loops (default: 120 = 2 minutes)
inactivity_timeout: 120
# =============================================================================
# Tool Loop Guardrails
# =============================================================================
# Soft warnings are enabled by default. They append guidance to repeated failed
# or non-progressing tool results but still let the tool execute. Hard stops are
# opt-in circuit breakers for autonomous/cron sessions where stopping a loop is
# preferable to spending the full iteration budget.
tool_loop_guardrails:
warnings_enabled: true
hard_stop_enabled: false
warn_after:
exact_failure: 2
same_tool_failure: 3
idempotent_no_progress: 2
hard_stop_after:
exact_failure: 5
same_tool_failure: 8
idempotent_no_progress: 5
# =============================================================================
# Context Compression (Auto-shrinks long conversations)
# =============================================================================
# When conversation approaches model's context limit, middle turns are
# automatically summarized to free up space while preserving important context.
#
# HOW IT WORKS:
# 1. Tracks actual token usage from API responses (not estimates)
# 2. When prompt_tokens >= threshold% of model's context_length, triggers compression
# 3. Protects first 3 turns (system prompt, initial request, first response)
# 4. Protects last N turns (default 20 messages = ~10 full turns of recent context)
# 5. Summarizes middle turns using a fast/cheap model
# 6. Inserts summary as a user message, continues conversation seamlessly
#
# Post-compression tail budget is target_ratio Γ— threshold Γ— context_length:
# 200K context, threshold 0.50, ratio 0.20 β†’ 20K tokens of recent tail preserved
# 1M context, threshold 0.50, ratio 0.20 β†’ 100K tokens of recent tail preserved
#
compression:
# Enable automatic context compression (default: true)
# Set to false if you prefer to manage context manually or want errors on overflow
enabled: true
# Opt-in compression progress notices on chat platforms (default: false).
# By design, routine automatic compression is SILENT on human-facing chat
# gateways (Telegram, Discord, Slack, ...) β€” it happens in the background
# with server-side logging only. Set true to also deliver the routine
# progress statuses (compacting started, preflight/pre-API compression,
# idle compaction, retry progress, and the compaction-complete notice) to
# chat platforms. Unrelated operational noise (auxiliary model failures,
# provider retry chatter) stays suppressed either way, and compression
# FAILURE notices + manual /compress feedback are always visible
# regardless of this setting. (#52995)
progress_notices: false
# Trigger compression at this % of model's context limit (default: 0.50 = 50%)
# Lower values = more aggressive compression, higher values = compress later
# Models with context windows below 512K are floored at 0.75 (raise-only) so
# compaction doesn't fire with half the window still free; set above 0.75 to override.
threshold: 0.50
# Per-model threshold overrides: keys are substring-matched against the model
# name (longest match wins). Useful when some models need different compaction
# points β€” e.g. a 1M-context model can compress later (0.30) while a 128K
# model needs to compress earlier (0.60). The small-context floor (75% for
# <512K models) still applies on top of per-model overrides.
# model_thresholds:
# "glm-5.2": 0.40
# "claude-sonnet": 0.35
# "gpt-5": 0.30
# Optional absolute token cap for the compression trigger (default: null = disabled).
# When set, compression fires at the LOWER of the ratio-based threshold and this
# absolute token count β€” first-fires-wins. It never fires later than this count
# regardless of which model is active (useful when switching between models with
# very different context windows). Clamped to the model's context length at
# apply-time, so a cap above the window is a no-op (ratio-based threshold wins).
# Survives model switches and fallback activations.
# threshold_tokens: 200000
# Existing Codex gpt-5.5 behavior: raise Hermes' compaction trigger to 85%
# for the ChatGPT Codex OAuth route. Set false to opt back down to threshold.
codex_gpt55_autoraise: true
# Fraction of the threshold to preserve as recent tail (default: 0.20 = 20%)
# e.g. 20% of 50% threshold = 10% of total context kept as recent messages.
# Summary output is separately capped at 12K tokens (Gemini output limit).
# Range: 0.10 - 0.80
target_ratio: 0.20
# Number of most-recent messages to always preserve (default: 20 β‰ˆ 10 full turns)
# Higher values keep more recent conversation intact at the cost of more aggressive
# compression of older turns.
protect_last_n: 20
# Minimum number of REAL (actionable) user messages guaranteed to survive in
# the uncompressed tail (default: 1 = the existing single last-user anchor,
# behavior-preserving). Raise to e.g. 3 to keep the last 3 real user turns
# verbatim even when bulky tool outputs fill the tail token budget β€” blank
# platform echoes, compaction handoffs, and synthetic continuation rows never
# count toward N. The tail can exceed the token budget when this pulls the
# cut back; the guarantee wins over the budget by design.
min_tail_user_messages: 1
# Compression retry rounds before a turn gives up with "max compression
# attempts reached" (default: 3, same as the previous hardcoded value).
# Raise (e.g. 6) for tool-schema-heavy sessions where 3 rounds cannot bring
# the request estimate under the threshold. Validated >= 1, hard cap 10.
max_attempts: 3
# Codex app-server (codex CLI runtime) thread-compaction mode. The codex
# agent owns the real thread context on this runtime, so Hermes' summarizer
# cannot shrink it β€” compaction goes through the app server instead.
# native = let Codex decide when to compact its own thread (default)
# hermes = let Hermes threshold trigger Codex thread/compact/start
# off = Hermes will not auto-trigger compaction; Codex may still compact natively
codex_app_server_auto: native
# Native OpenAI Responses server-side compaction (default: false). When true,
# gpt-5.6-family models on the DIRECT OpenAI API (api.openai.com) or a ChatGPT
# Codex subscription compact server-side: OpenAI prunes older context into an
# encrypted checkpoint that Hermes replays on later turns. No other provider,
# route, or model is affected. Hermes' local compression stays armed as the
# fallback and still handles every non-eligible session.
codex_responses_native: false
# Server-side compaction trigger in input tokens. Clamped below the local
# compression threshold at request time so the server compacts first.
codex_responses_compact_threshold: 200000
# Number of non-system messages to protect at the head of the transcript, in
# ADDITION to the system prompt (which is always implicitly protected).
# Head messages are NEVER summarized β€” they survive every compression
# indefinitely. This gives stable early context for short/medium sessions,
# but in long-running sessions that rely on rolling compaction the pinned
# opening turns may not match how you want the session framed over time.
# Set to 0 to preserve ONLY the system prompt (plus the rolling summary
# and recent tail) β€” the cleanest configuration for long-running sessions.
# Default 3 preserves the system prompt plus the first three non-system
# head messages, matching the pre-feature behaviour.
protect_first_n: 3
# Idle compaction (default: 0 = disabled). When > 0, a session that resumes
# after at least this many seconds of inactivity compacts its accumulated
# history up front, before the first reply, so a long-lived thread you come
# back to later doesn't re-read its full stale context on every turn.
# Time-based, so it complements (does not replace) the size-based `threshold`
# above. It is skipped when the context is already small (at or below the
# post-compression target = threshold Γ— target_ratio), so it never wastes a
# summarization on a short idle thread. Example: 1800 = compact after 30 min idle.
idle_compact_after_seconds: 0
# Proactive tool-result prune (default: 0 = disabled). Opt-in token trigger
# for a deterministic, no-LLM prune of OLD tool-result payloads, run
# independently of `threshold` above. On large-window models (512K/1M) the
# ratio threshold rarely fires, so bulky tool outputs (terminal dumps, file
# reads, web extracts) ride along in history and get re-billed every turn.
# When re-sent history exceeds this many tokens, the prune dedupes identical
# results, summarizes older oversized ones, and truncates large tool-call
# arguments β€” protecting the most recent `protect_last_n` messages and never
# calling the model. Try 48000 to enable. Built-in compressor engine only;
# other context engines inherit a safe no-op.
# NOTE: a committed prune rewrites already-sent history, which invalidates
# the provider's prompt-cache prefix β€” the min_reclaim gate below keeps
# those cache breaks episodic (like a compression boundary) instead of
# per-turn.
proactive_prune_tokens: 0
# The prune's summarize pass only touches tool results larger than this many
# characters (clamped to >= 200 so a generated summary can't be
# re-summarized). Default 8000.
proactive_prune_min_result_chars: 8000
# A proactive prune only COMMITS when it reclaims at least this many tokens
# (measured on the pruned output). This is the prompt-cache hysteresis gate:
# one meaningful, amortized cache break per batch of stale tool output
# instead of a tiny break on every tool iteration. 0 = commit any non-zero
# prune. Default 4096.
proactive_prune_min_reclaim_tokens: 4096
# To pin a specific model/provider for compression summaries, use the
# auxiliary section below (auxiliary.compression.provider / model).
# =============================================================================
# Anthropic prompt caching TTL
# =============================================================================
# When prompt caching is active (Claude via OpenRouter or native Anthropic),
# Anthropic supports two TTL tiers for cached prefixes: "5m" (default) and
# "1h". Other values are ignored and "5m" is used.
#
prompt_caching:
cache_ttl: "5m" # use "1h" for long sessions with pauses between turns
# =============================================================================
# Auxiliary Models (Advanced β€” Experimental)
# =============================================================================
# Hermes uses lightweight "auxiliary" models for side tasks: image analysis,
# browser screenshot analysis, web page summarization, TTS audio-tag insertion,
# and context compression.
#
# By default these use Gemini Flash via OpenRouter or Nous Portal and are
# auto-detected from your credentials. You do NOT need to change anything
# here for normal usage.
#
# WARNING: Overriding these with providers other than OpenRouter or Nous Portal
# is EXPERIMENTAL and may not work. Not all models/providers support vision,
# produce usable summaries, or accept the same API format. Change at your own
# risk β€” if things break, reset to "auto" / empty values.
#
# Each task has its own provider + model pair so you can mix providers.
# For example: OpenRouter for vision (needs multimodal), but your main
# local endpoint for compression (just needs text).
#
# Provider options:
# "auto" - Best available: OpenRouter β†’ Nous Portal β†’ main endpoint (default)
# "openrouter" - Force OpenRouter (requires OPENROUTER_API_KEY)
# "nous" - Force Nous Portal (requires: hermes auth add nous)
# "gemini" - Force Google AI Studio direct (requires: GOOGLE_API_KEY or GEMINI_API_KEY)
# "ollama-cloud" - Ollama Cloud (requires: OLLAMA_API_KEY)
# "codex" - Force Codex OAuth (requires: hermes model β†’ Codex).
# Uses gpt-5.3-codex which supports vision.
# "main" - Use your custom endpoint (OPENAI_BASE_URL + OPENAI_API_KEY).
# Works with OpenAI API, local models, or any OpenAI-compatible
# endpoint. Also falls back to Codex OAuth and API-key providers.
#
# Model: leave empty to use the provider's default. When empty, OpenRouter
# uses "google/gemini-3-flash-preview" and Nous uses "gemini-3-flash".
# Other providers pick a sensible default automatically.
#
# auxiliary:
# # Image analysis: vision_analyze tool + browser screenshots
# vision:
# provider: "auto"
# model: "" # e.g. "google/gemini-2.5-flash", "openai/gpt-4o"
# timeout: 30 # LLM API call timeout (seconds)
# download_timeout: 30 # Image HTTP download timeout (seconds)
# # Increase for slow connections or self-hosted image servers
# reasoning_effort: "" # Per-task thinking level: none, minimal, low, medium,
# # high, xhigh, max, ultra. Empty = provider default.
# # Works on every auxiliary task block (vision,
# # web_extract, compression, title_generation, curator,
# # background_review, moa_reference, ...). Example: run
# # compression at "low" and vision at "none" to cut
# # side-task latency/cost on reasoning models.
#
# # Web page scraping / summarization + browser page text extraction
# web_extract:
# provider: "auto"
# model: ""
# reasoning_effort: "low"
#
# # Gemini 3.1 TTS hidden audio-tag insertion
# tts_audio_tags:
# provider: "auto" # empty model = your main chat model
# model: ""
# timeout: 30
#
# # Automatic session title generation after the first exchange
# title_generation:
# enabled: true # set false to disable auto-title generation
# provider: "auto"
# model: ""
# timeout: 30
# language: "" # empty = match the user's language; or e.g. "English"
#
# # Session search β€” summarizes matching past sessions
# session_search:
# provider: "auto"
# model: ""
# timeout: 30
# max_concurrency: 3 # Limit parallel summaries to reduce request-burst 429s
# extra_body: {} # Provider-specific OpenAI-compatible request fields
# # Example for providers that support request-body
# # reasoning controls:
# # extra_body:
# # enable_thinking: false
# # Some vLLM/Qwen deployments expect this nested:
# # extra_body:
# # chat_template_kwargs:
# # enable_thinking: false
#
# # Auto-generated short session titles after the first exchange.
# # Each active Discord/Telegram channel can spawn a background title
# # call. Cap concurrency to keep retries during provider incidents
# # from amplifying the request burst. Leave unset for legacy behavior
# # (unlimited).
# title_generation:
# provider: "auto"
# model: ""
# # max_concurrency: 2 # Optional: cap simultaneous title calls
#
# # Context compression β€” summarizes long sessions to shrink the prompt.
# # Heavy and often hits the slowest provider chain. Setting a small
# # cap prevents many sessions from compressing simultaneously during
# # provider degradation. Leave unset for legacy behavior (unlimited).
# compression:
# provider: "auto"
# model: ""
# # max_concurrency: 2 # Optional: cap simultaneous compression calls
#
# # Post-turn memory/skill self-improvement review fork. Runs after a turn
# # when the nudge intervals fire; writes skills/memories in a daemon thread.
# # Usage is recorded under session_model_usage task='background_review'.
# background_review:
# enabled: true # false = skip automatic forks (/refine still works)
# provider: "auto" # or pin a cheaper model (see memory.md)
# model: ""
# =============================================================================
# Persistent Memory
# =============================================================================
# Bounded curated memory injected into the system prompt every session.
# Two stores: MEMORY.md (agent's notes) and USER.md (user profile).
# Character limits keep the memory small and focused. The agent manages
# pruning -- when at the limit, it must consolidate or replace entries.
# Disabled by default in batch_runner.
#
memory:
# Agent's personal notes: environment facts, conventions, things learned
memory_enabled: true
# User profile: preferences, communication style, expectations
user_profile_enabled: true
# Character limits (~2.75 chars per token, model-independent)
memory_char_limit: 2200 # ~800 tokens
user_char_limit: 1375 # ~500 tokens
# Periodic memory nudge: remind the agent to consider saving memories
# every N user turns. Set to 0 to disable. Only active when memory is enabled.
nudge_interval: 10 # Nudge every 10 user turns (0 = disabled)
# Memory flush: give the agent one turn to save memories before context is
# lost (compression, /new, /reset, exit). Set to 0 to disable.
# For exit/reset, only fires if the session had at least this many user turns.
flush_min_turns: 6 # Min user turns to trigger flush on exit/reset (0 = disabled)
# =============================================================================
# Session Reset Policy (Messaging Platforms)
# =============================================================================
# Controls when messaging sessions (Telegram, Discord, WhatsApp, Slack) are
# automatically cleared. Default is "none": sessions never auto-reset β€”
# conversation context lives until you /reset or /new manually, or context
# compression kicks in. Opt in to automatic resets if you prefer sessions to
# clear on a schedule (long-lived context increases API cost per message,
# though prompt caching and compression keep this manageable).
#
# When an automatic reset triggers, the agent first saves important
# information to its persistent memory β€” but the conversation context is
# wiped. The agent starts fresh but retains learned facts via its memory
# system.
#
# Modes:
# "none" - Never auto-reset (default); context lives until /reset or compression
# "idle" - Reset after N minutes of inactivity
# "daily" - Reset at a fixed hour each day
# "both" - Reset on EITHER inactivity timeout or daily boundary
#
session_reset:
mode: none # "none", "idle", "daily", or "both"
idle_minutes: 1440 # Inactivity timeout in minutes (used by "idle"/"both")
at_hour: 4 # Daily reset hour, 0-23 local time (used by "daily"/"both")
# Maximum number of simultaneously active chat sessions across CLI, TUI,
# dashboard chat, and messaging gateway. Set to null, 0, or omit to allow
# unlimited concurrent sessions. When the limit is reached, new sessions get a
# clean error while existing active sessions keep their normal behavior. This
# top-level key takes precedence over gateway.max_concurrent_sessions. The cap
# is a best-effort single-host/profile runtime guard; Hermes fails open if the
# local runtime lease registry cannot be read or locked.
max_concurrent_sessions: null
# When true, group/channel chats use one session per participant when the platform
# provides a user ID. This is the secure default and prevents users in the same
# room from sharing context, interrupts, and token costs. Set false only if you
# explicitly want one shared "room brain" per group/channel.
group_sessions_per_user: true
# ─────────────────────────────────────────────────────────────────────────────
# API Server β€” per-client model routing
# ─────────────────────────────────────────────────────────────────────────────
# Route different API clients to different models/providers on a single
# Hermes deployment. Clients choose a backend by sending a specific string
# as the OpenAI ``model`` field. Unmapped model values fall back to the
# global model configured in the ``model:`` section above, and an explicit
# session /model override always wins over a route.
#
# Configure via the ``platforms.api_server.extra.model_routes`` gateway
# config block:
#
# platforms:
# api_server:
# enabled: true
# extra:
# key: "your-api-server-secret"
# model_routes:
# # Xiaozhi clients send model="minimax-m2" β†’ routed to MiniMax via OpenRouter
# minimax-m2:
# model: "minimax/minimax-m1"
# provider: "openrouter" # optional β€” overrides global provider
# # api_key: "sk-..." # optional β€” per-route UPSTREAM provider
# # key (NOT caller auth; never logged)
# # base_url: "https://..." # optional β€” per-route base URL
# # GPT clients keep their own alias
# gpt-5:
# model: "openai/gpt-5"
# provider: "openrouter"
#
# Configured aliases are automatically listed by GET /v1/models so clients
# can discover them without manual coordination. Caller authentication is
# unchanged: every request still authenticates with the global API server
# key (``extra.key`` / API_SERVER_KEY).
# ─────────────────────────────────────────────────────────────────────────────
# Gateway Streaming
# ─────────────────────────────────────────────────────────────────────────────
# Stream tokens to messaging platforms in real-time. The bot sends a message
# on first token, then progressively edits it as more tokens arrive.
# Disabled by default β€” enable to try the streaming UX on Telegram/Discord/Slack.
# For Telegram, partial edits are sent as plain text and only the final edit uses MarkdownV2.
streaming:
enabled: false
# transport: edit # "edit" = progressive editMessageText
# edit_interval: 0.3 # seconds between message edits
# buffer_threshold: 40 # chars before forcing an edit flush
# cursor: " β–‰" # cursor shown during streaming
# =============================================================================
# Skills Configuration
# =============================================================================
# Skills are reusable procedures the agent can load and follow. The agent can
# also create new skills after completing complex tasks.
#
skills:
# Nudge the agent to create skills after complex tasks.
# Every N tool-calling iterations, remind the model to consider saving a skill.
# Set to 0 to disable.
creation_nudge_interval: 15
# External skill directories β€” share skills across tools/agents without
# copying them into ~/.hermes/skills/. Each path is expanded (~ and ${VAR})
# and resolved to an absolute path. External dirs are read-only: skill
# creation always writes to ~/.hermes/skills/. Local skills take precedence
# when names collide.
# external_dirs:
# - ~/.agents/skills
# - /home/shared/team-skills
# =============================================================================
# Agent Behavior
# =============================================================================
agent:
# Maximum tool-calling iterations per conversation (default: 500)
# Higher = more room for complex tasks, but costs more tokens
# Recommended: 20-30 for focused tasks, 50-100 for open exploration
max_turns: 500
# Inactivity timeout for gateway agent runs (seconds, 0 = unlimited).
# The agent can run indefinitely when actively calling tools or receiving
# API responses. Only fires after the agent has been idle for this duration.
# gateway_timeout: 1800
# Maximum time an alias routing key waits for an active turn holding the same
# resolved session lease. On expiry Hermes rejects this inbound message and
# asks the user to resend rather than running it without serialization.
# Non-positive values fall back to the 1800-second default.
# gateway_turn_lease_timeout: 1800
# Staged warning: send a warning before escalating to full timeout.
# Fires once per run when inactivity reaches this threshold (seconds).
# Set to 0 to disable the warning.
# gateway_timeout_warning: 900
# Session stall watchdog (seconds). When a busy session has a pending
# inbound follow-up and the agent activity clock is idle this long, the
# gateway logs a WARNING and notifies the user to try /new. Does not kill
# the turn (see gateway_timeout). 0 = disable. Default 300.
# session_stall_timeout: 300
# Related in-agent compression timeouts (they live under the top-level
# compression: block, shown here for discoverability next to the stall
# watchdog they complement β€” a hung compression is a common stall cause):
# compression:
# context_timeout_seconds: 120 # inactivity budget for in-agent
# # compress_context (0 = disable)
# context_total_ceiling_seconds: 600 # absolute cap on the pre-commit
# # wait even while tokens stream
# Graceful drain timeout for gateway stop/restart (seconds).
# Default 0 = no drain: a restart interrupts in-flight agents immediately,
# cleans up, and exits. Set a positive value only if you want a grace
# window on /restart, and keep it well under systemd's TimeoutStopSec.
# restart_drain_timeout: 0
# Cron-only floor under the same drain (seconds). Default 30.
# restart_drain_timeout above is written for chat turns, which are cheap to
# interrupt: the user is told the gateway is restarting and the session
# resumes on their next message. A cron run has no such safety net β€” it is
# recorded in jobs.json as a permanent failure, nobody is waiting on it, and
# a recurring job simply skips to its next schedule. So in-flight cron work
# gets its own grace window instead of inheriting the 0 above.
# Clamped at runtime to the shutdown-watchdog leash (restart_drain_timeout
# + 60s) minus teardown headroom, so values past ~50s need a matching
# TimeoutStopSec bump to take effect. Set 0 to opt out and drain cron on
# restart_drain_timeout like before.
# cron_drain_timeout: 30
# Upper bound (seconds) a submitted prompt waits for the deferred agent
# build (MCP discovery, model metadata, skills scan) before failing with a
# visible error. The wait is patient β€” the message is delivered as soon as
# the build completes, and a progress notice is shown past 30s β€” so this cap
# only fires on a genuinely hung build. Raise it for deployments with many
# slow or unreachable MCP servers. Default 600.
# build_wait_timeout: 600
# Max app-level retry attempts for API errors (connection drops, provider
# timeouts, 5xx, etc.) before the agent surfaces the failure. Lower this
# to 1 if you use fallback providers and want fast failover on flaky
# primaries (default 3). The OpenAI SDK does its own low-level retries
# underneath this wrapper β€” this is the Hermes-level loop.
# api_max_retries: 3
# After the agent edits code without fresh passing verification, nudge it to
# verify before finishing. The default "auto" enables it on interactive
# coding surfaces (CLI, TUI, desktop) and programmatic callers, and disables
# it on conversational messaging surfaces (Telegram, Discord, etc.) where the
# verification summary would reach a human as chat noise. Set true or false to
# force it on or off; the HERMES_VERIFY_ON_STOP env var (1/0) takes precedence.
# verify_on_stop: auto
# Standing operator instructions for the coding posture (when Hermes is in a
# code workspace). Appended to the coding brief as an extra system block, so
# you can pin project-wide workflow rules without editing the shipped brief.
# Accepts a string or a list of strings. Takes effect next session.
# coding_instructions:
# - "For UI work, don't run tsc/lint until I approve the look."
# - "Clean the diff before you commit and push."
# When verify-on-stop finds edited code without fresh verification evidence,
# append guidance for creative UI work (avoid broad tsc/lint/test before visual
# approval) and clean-diff expectations. Set false to keep that nudge terse.
# verify_guidance: true
# A `pre_verify` hook (plugin or shell, see Event Hooks docs) can keep the
# agent going one more turn to verify/clean before finishing. This caps how
# many times one turn may be nudged to continue, so a hook can't trap the loop.
# Default 3.
# max_verify_nudges: 3
# Enable verbose logging
verbose: false
# Reasoning effort level (OpenRouter and Nous Portal)
# Controls how much "thinking" the model does before responding.
# Options: "xhigh" (max), "high", "medium", "low", "minimal", "none" (disable)
reasoning_effort: "medium"
# Per-model reasoning effort overrides (optional dict)
# Key: any sensible model spelling works (exact, dots↔dashes interchangeable,
# provider prefix optional). First match wins.
# Value: reasoning effort level (same options as reasoning_effort)
# Override the global reasoning_effort for that specific model.
# NOTE: no `hermes config set` support for this key -- edit YAML directly.
# reasoning_overrides:
# "openrouter/anthropic/claude-opus-4.5": "xhigh"
# "openai/gpt-5": "low"
# "claude-opus-4.6": "high" # bare model name also works
# "deepseek/deepseek-v4-pro": "xhigh" # dots and dashes are interchangeable
reasoning_overrides: {}
# Custom personalities (use with /personality command).
# Built-ins (helpful, concise, technical, creative, teacher, kawaii, catgirl,
# pirate, shakespeare, surfer, noir, uwu, philosopher, hype) are always
# available on every surface β€” defined once in hermes_cli/personality.py.
# Entries here ADD new personalities or OVERRIDE a built-in by name.
# The active selection is stored in display.personality (never here, and
# never in agent.system_prompt β€” that field is your manual system prompt).
personalities: {}
# mentor: "You are a supportive mentor. Guide, don't lecture."
# reviewer:
# system_prompt: "You are a meticulous code reviewer."
# tone: "direct"
# style: "terse"
# =============================================================================
# Toolsets
# =============================================================================
# Control which tools the agent has access to.
# Use `hermes tools` to interactively enable/disable tools per platform.
# =============================================================================
# Platform Toolsets (per-platform tool configuration)
# =============================================================================
# Override which toolsets are available on each platform.
# If a platform isn't listed here, its built-in default is used.
#
# You can use EITHER:
# - A preset like "hermes-cli" or "hermes-telegram" (curated tool set)
# - A list of individual toolsets to compose your own (see list below)
#
# Supported platform keys: cli, telegram, discord, whatsapp, slack, qqbot, teams, google_chat
#
# Examples:
#
# # Use presets (same as defaults):
# platform_toolsets:
# cli: [hermes-cli]
# telegram: [hermes-telegram]
#
# # Custom: give Telegram only web + terminal + file + planning:
# platform_toolsets:
# telegram: [web, terminal, file, todo]
#
# # Custom: CLI without browser or image gen:
# platform_toolsets:
# cli: [web, terminal, file, skills, todo, tts, cronjob]
#
# # Restrictive: Discord gets read-only tools only:
# platform_toolsets:
# discord: [web, vision, skills, todo]
#
# If not set, defaults are:
# cli: hermes-cli (everything + cronjob management)
# telegram: hermes-telegram (terminal, file, web, vision, image, tts, browser, skills, todo, cronjob, messaging)
# discord: hermes-discord (same as telegram)
# whatsapp: hermes-whatsapp (same as telegram)
# slack: hermes-slack (same as telegram)
# signal: hermes-signal (same as telegram)
# homeassistant: hermes-homeassistant (same as telegram)
# qqbot: hermes-qqbot (same as telegram)
# teams: hermes-teams (same as telegram)
# google_chat: hermes-google_chat (same as telegram)
#
platform_toolsets:
cli: [hermes-cli]
telegram: [hermes-telegram]
discord: [hermes-discord]
whatsapp: [hermes-whatsapp]
slack: [hermes-slack]
signal: [hermes-signal]
homeassistant: [hermes-homeassistant]
qqbot: [hermes-qqbot]
yuanbao: [hermes-yuanbao]
teams: [hermes-teams]
google_chat: [hermes-google_chat]
# =============================================================================
# Gateway Platform Settings
# =============================================================================
# Optional per-platform messaging settings.
# Platform-specific knobs live under `extra`.
#
# platforms:
# telegram:
# reply_to_mode: "first" # off | first | all
# # guest_mode lets explicit @mentions from non-allowlisted groups through.
# # Default false; ordinary messages, replies, and regex wake words stay blocked.
# guest_mode: false
# # allowed_chats: ["-1001234567890"]
# extra:
# disable_link_previews: false # Set true to suppress Telegram URL previews in bot messages
# rich_messages: false # Bot API 10.1 rich messages (tables/task lists/details/math); default false for copyable legacy MarkdownV2, set true to opt in
# rich_drafts: false # Experimental rich draft previews during Telegram DM streaming; default false because Telegram Desktop/macOS can visually overlay draft frames
# command_menu:
# # Telegram allows up to 100 BotCommands; Hermes defaults to 60 so
# # all built-in commands plus common skill commands stay visible
# # while remaining under Telegram's payload-size limit. Clamped 1..100.
# max_commands: 60
# # prepend = user priority first, then Hermes defaults
# # append = Hermes defaults first, then user priority
# # replace = only the list below defines priority
# priority_mode: prepend
# priority:
# - my_plugin_command
# slack:
# extra:
# # Render live tool calls as Slack-native plan/task cards. This explicit
# # opt-in works even though Slack text tool_progress defaults to off.
# native_task_cards: false
# webhook:
# extra:
# # Route scripts default to a 30 second timeout. Scripts must live under
# # the active profile's scripts directory and receive webhook JSON on stdin.
# script_timeout_seconds: 30
#
# Discord-specific settings (config.yaml top-level, not under platforms:):
#
# discord:
# require_mention: true # Require @mention in server channels (default: true)
# auto_thread: true # Auto-create thread on @mention (default: true)
# free_response_channels: "" # Channel IDs where no mention is needed
# reactions: true # Show processing reactions (default: true)
# history_backfill: true # Recover missed channel messages on mention (default: true)
# history_backfill_limit: 50 # Max messages to scan backwards (default: 50)
# ─────────────────────────────────────────────────────────────────────────────
# Available toolsets (use these names in platform_toolsets or the toolsets list)
#
# Run `hermes chat --list-toolsets` to see all toolsets and their tools.
# Run `hermes chat --list-tools` to see every individual tool with descriptions.
# ─────────────────────────────────────────────────────────────────────────────
#
# INDIVIDUAL TOOLSETS (compose your own):
# web - web_search, web_extract
# search - web_search only (no scraping)
# terminal - terminal, process
# file - read_file, write_file, patch, search
# browser - browser_navigate, browser_snapshot, browser_click, browser_type,
# browser_scroll, browser_back, browser_press,
# browser_get_images, browser_vision (requires BROWSERBASE_API_KEY)
# vision - vision_analyze (requires OPENROUTER_API_KEY)
# image_gen - image_generate (requires FAL_KEY)
# skills - skills_list, skill_view
# skills_hub - skill_hub (search/install/manage from online registries β€” user-driven only)
# todo - todo (in-memory task planning, no deps)
# tts - text_to_speech (Edge TTS free, or ELEVENLABS/OPENAI/MINIMAX/MISTRAL key)
# cronjob - cronjob (create/list/update/pause/resume/run/remove scheduled tasks)
#
# PRESETS (curated bundles):
# hermes-cli - All of the above except send_message
# hermes-telegram - terminal, file, web, vision, image_gen, tts, browser,
# skills, todo, cronjob, send_message
# hermes-discord - Same as hermes-telegram
# hermes-whatsapp - Same as hermes-telegram
# hermes-slack - Same as hermes-telegram
#
# COMPOSITE:
# debugging - terminal + web + file
# safe - web + vision (no terminal access)
# all - Everything available
#
# web - Web search and content extraction (web_search, web_extract)
# search - Web search only, no scraping (web_search)
# terminal - Command execution and process management (terminal, process)
# file - File operations: read, write, patch, search
# browser - Full browser automation (navigate, click, type, screenshot, etc.)
# vision - Image analysis (vision_analyze)
# image_gen - Image generation with FLUX (image_generate)
# skills - Load skill documents (skills_list, skill_view)
# todo - Task planning and tracking for multi-step work
# memory - Persistent memory across sessions (personal notes + user profile)
# session_search - Search and recall past conversations (FTS5 + Gemini Flash summarization)
# tts - Text-to-speech (Edge TTS free, ElevenLabs, OpenAI, MiniMax, Mistral)
# cronjob - Schedule and manage automated tasks (CLI-only)
#
# Composite toolsets:
# debugging - terminal + web + file (for troubleshooting)
# safe - web + vision (no terminal access)
# NOTE: The top-level "toolsets" key is deprecated and ignored.
# Tool configuration is managed per-platform via platform_toolsets above.
# Use `hermes tools` to configure interactively, or edit platform_toolsets directly.
#
# CLI override: hermes chat --toolsets terminal,web,file
# =============================================================================
# MCP (Model Context Protocol) Servers
# =============================================================================
# Connect to external MCP servers to add tools from the MCP ecosystem.
# Each server's tools are automatically discovered and registered.
# See website/docs/user-guide/features/mcp.md for full documentation.
#
# Stdio servers (spawn a subprocess):
# command: the executable to run
# args: command-line arguments
# env: environment variables (only these + safe defaults passed to subprocess)
#
# HTTP servers (connect to a URL):
# url: the MCP server endpoint
# headers: HTTP headers (e.g., for authentication)
#
# Optional per-server settings:
# timeout: tool call timeout in seconds (default: 120)
# connect_timeout: initial connection timeout (default: 60)
# keepalive_interval: liveness ping cadence in seconds (default: 180).
# Lower it below the server's session TTL for servers that expire idle
# sessions quickly (e.g. Unreal Engine editor MCP, ~15s), otherwise idle
# tool calls hit an expired session and pay a slow reconnect. Floored at 5s.
#
# mcp_servers:
# time:
# command: uvx
# args: ["mcp-server-time"]
# filesystem:
# command: npx
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
# notion:
# url: https://mcp.notion.com/mcp
# github:
# command: npx
# args: ["-y", "@modelcontextprotocol/server-github"]
# env:
# GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..."
#
# Sampling (server-initiated LLM requests) β€” enabled by default.
# Per-server config under the 'sampling' key:
# analysis:
# command: npx
# args: ["-y", "analysis-server"]
# sampling:
# enabled: true # default: true
# model: "gemini-3-flash" # override model (optional)
# max_tokens_cap: 4096 # max tokens per request
# timeout: 30 # LLM call timeout (seconds)
# max_rpm: 10 # max requests per minute
# allowed_models: [] # model whitelist (empty = all)
# max_tool_rounds: 5 # tool loop limit (0 = disable)
# log_level: "info" # audit verbosity
# =============================================================================
# Text-to-Speech
# =============================================================================
# TTS defaults to Edge TTS unless changed in ~/.hermes/config.yaml.
# Gemini TTS supports persona/director prompt files, and Gemini 3.1 Flash TTS
# can use a hidden auxiliary rewrite pass to insert expressive square-bracket
# audio tags into the TTS script without showing tags in chat.
#
# tts:
# provider: "gemini"
# speed: 1.0 # global speed multiplier (provider-specific overrides this)
# gemini:
# model: "gemini-3.1-flash-tts-preview"
# voice: "Kore"
# audio_tags: false
# persona_prompt_file: "" # e.g. ~/.hermes/tts/radio-host.md
# xai:
# voice_id: "eve" # built-in or custom voice ID from xAI Console
# language: "en" # BCP-47 code ("en", "pt-BR") or "auto"
# speed: 1.0 # 0.7-1.5 playback speed
# auto_speech_tags: false # insert expressive audio tags via LLM rewrite
# text_normalization: false # normalize numbers/abbreviations/symbols
# optimize_streaming_latency: 0 # 0-2, trades quality for lower latency
# sample_rate: 24000 # 22050 / 24000 / 44100 / 48000
# bit_rate: 128000 # MP3 bitrate (codec=mp3 only)
# =============================================================================
# Voice Transcription (Speech-to-Text)
# =============================================================================
# Automatically transcribe voice messages on messaging platforms.
# Providers: local (free, faster-whisper) | groq (free tier) | openai (Whisper API) | mistral (Voxtral Transcribe)
# Set the corresponding API key in .env: GROQ_API_KEY, OPENAI_API_KEY, or MISTRAL_API_KEY.
stt:
enabled: true
# provider: "local" # auto-detected if omitted
# --- Cloud pre-upload silence trim (groq/openai/mistral/xai/elevenlabs/deepinfra) ---
# Local whisper gets Silero VAD; cloud endpoints otherwise receive raw audio β€”
# silence inflates upload time, per-audio-minute billing, and hallucination risk.
# Collapses pauses with ffmpeg client-side; clips under 12s skip the trim, and
# on any failure the original uploads untouched.
# cloud_trim_silence: true # set false to always upload the original audio
# cloud_trim_threshold_db: -40 # audio quieter than this counts as silence
# cloud_trim_keep_ms: 300 # how much of each pause survives (keeps natural pacing)
# Optional static transcription prompt: vocabulary/context hints for
# prompt-capable backends. Composition is deterministic: this config value
# is the base, then `pre_transcription` hooks run in registration order with
# last-writer-wins semantics per field.
#
# Privacy: the final prompt is sent to the configured STT provider alongside
# the audio. Do not include secrets or session context you would not send to
# that provider.
#
# Length / truncation contract: Whisper-family backends (local, openai,
# groq, deepinfra) only use the final ~224 prompt tokens, so Hermes
# truncates longer prompts client-side (keeping the tail) with a WARNING
# log β€” never an error. Other backends own their own validation.
#
# Provider behavior:
# local (faster-whisper) -> `initial_prompt`, forwarded unchanged
# openai / groq -> `prompt`, forwarded unchanged
# mistral -> `prompt`, forwarded unchanged
# deepinfra -> OpenAI-compatible `prompt`, unchanged
# local_command/xai/
# elevenlabs -> unsupported; DEBUG log, then continue
# plugin providers -> `prompt` in `**extra`; plugin owns limits
# prompt: "Hermes, Teknium, Nous Research, kanban"
local:
model: "base" # tiny | base | small | medium | large-v3 | turbo
# language: "" # auto-detect; set to "en", "es", "fr", etc. to force
# initial_prompt: "" # Optional faster-whisper prompt, e.g. bias Chinese output to simplified Chinese
# --- Anti-hallucination hardening (whisper decodes junk from silence without these) ---
# vad: true # Silero VAD filter (default on) β€” silence never reaches whisper.
# # Set false to restore raw behavior (e.g. transcribing music/ambient audio).
# vad_min_silence_ms: 500 # min silence (ms) that splits speech chunks when vad is on
# no_speech_prob_threshold: 0.6 # drop a segment only if no_speech_prob > this...
# logprob_threshold: -1.0 # ...AND avg_logprob < this (both must hit β€” quiet real speech survives)
# unload_after_idle_seconds: 0 # 0=never unload (default); e.g. 300 releases the model after 5min idle (frees VRAM on GPU; next voice message reloads it)
language: "en" # GLOBAL language hint for every STT provider (per-provider language wins). Set "" for auto-detect.
# groq:
# model: "whisper-large-v3-turbo"
# language: "" # blank = stt.language > HERMES_LOCAL_STT_LANGUAGE > auto-detect
openai:
model: "whisper-1" # whisper-1 | gpt-4o-mini-transcribe | gpt-4o-transcribe | gpt-transcribe
language: "" # auto-detect; set to "en", "es", "fr", etc. to force
# mistral:
# model: "voxtral-mini-latest" # voxtral-mini-latest | voxtral-mini-2602
# deepinfra:
# # Model id is discovered live from the DeepInfra catalog filtered
# # by the `stt` surface tag β€” leave `model` blank to take the first
# # live result. Pin only when you need a specific Whisper variant.
# model: ""
# Text-to-speech. Only the deepinfra block is documented here β€” the
# remaining providers (edge, openai, xai, minimax, mistral, gemini,
# elevenlabs, neutts, kittentts, piper) inherit sensible defaults from
# DEFAULT_CONFIG in hermes_cli/config.py.
# tts:
# provider: "deepinfra"
# deepinfra:
# # Model id is discovered live from the DeepInfra catalog filtered
# # by the `tts` surface tag β€” leave `model` blank to take the first
# # live result.
# model: ""
# voice: "default"
# "Hey Hermes" hands-free wake word (off by default; toggle with /wake).
# Full engine/provider options inherit from DEFAULT_CONFIG in
# hermes_cli/config_defaults.py β€” only capture placement is documented here.
# wake_word:
# enabled: false
# capture: "auto" # auto | local | client
# # auto: backend PortAudio mic when one exists; a remote
# # desktop on a mic-less (headless/VPS) backend
# # streams its own mic via the wake.feed RPC.
# # local: always the backend mic (wake_word.input_device).
# # client: always desktop-streamed PCM (detection stays
# # on the backend).
# Image generation. Each provider plugin reads its own ``image_gen.<name>``
# block; deepinfra discovers models live from
# api.deepinfra.com/v1/openai/models filtered by the ``image-gen`` tag β€”
# no model id is hardcoded, so retired models disappear automatically.
# image_gen:
# provider: "deepinfra"
# deepinfra:
# # Leave `model` blank for the first live `image-gen`-tagged result.
# model: ""
# =============================================================================
# Response Pacing (Messaging Platforms)
# =============================================================================
# Add human-like delays between message chunks.
# human_delay:
# mode: "off" # "off" | "natural" | "custom"
# min_ms: 800 # Min delay (custom mode only)
# max_ms: 2500 # Max delay (custom mode only)
# =============================================================================
# Session Logging
# =============================================================================
# Session trajectories are automatically saved to logs/ directory.
# Each session creates: logs/session_YYYYMMDD_HHMMSS_UUID.json
#
# The session ID is displayed in the welcome banner for easy reference.
# Logs contain full conversation history in trajectory format:
# - System prompt, user messages, assistant responses
# - Tool calls with inputs/outputs
# - Timestamps for debugging
#
# No configuration needed - logging is always enabled.
# To disable, you would need to modify the source code.
# =============================================================================
# Code Execution Sandbox (Programmatic Tool Calling)
# =============================================================================
# The execute_code tool runs Python scripts that call Hermes tools via RPC.
# Intermediate tool results stay out of the LLM's context window.
code_execution:
timeout: 300 # Max seconds per script before kill (default: 300 = 5 min)
max_tool_calls: 50 # Max RPC tool calls per execution (default: 50)
# =============================================================================
# Subagent Delegation
# =============================================================================
# The delegate_task tool spawns child agents with isolated context.
# Supports single tasks and batch mode (default 3 parallel, configurable).
delegation:
max_iterations: 250 # Max tool-calling turns per child (default: 250)
# max_concurrent_children: 10 # Max parallel child agents per batch (default: 10, floor: 1, no ceiling).
# WARNING: values above 10 multiply API cost linearly.
# max_spawn_depth: 1 # Delegation tree depth cap (range: 1-3, default: 1 = flat).
# Raise to 2 to allow workers to spawn their own subagents.
# Requires role="orchestrator" on intermediate agents.
# orchestrator_enabled: true # Kill switch for role="orchestrator" children (default: true).
# subagent_auto_approve: false # When a subagent hits a dangerous-command approval prompt, auto-deny (default: false)
# or auto-approve "once" (true) instead of blocking on stdin.
# The parent TUI owns stdin, so blocking would deadlock; non-interactive resolution is required.
# Both choices emit a logger.warning audit line. Flip to true only for cron/batch pipelines.
# inherit_mcp_toolsets: true # When explicit child toolsets are narrowed, also keep the parent's MCP toolsets (default: true). Set false for strict intersection.
# model: "google/gemini-3-flash-preview" # Override model for subagents (empty = inherit parent)
# provider: "openrouter" # Override provider for subagents (empty = inherit parent)
# # Resolves full credentials (base_url, api_key) automatically.
# # Supported: openrouter, nous, zai, kimi-coding, minimax
# # Cost tip: keep the parent on a frontier model and pin
# # delegation.model to an inexpensive one β€” children carry the
# # vast majority of tokens, so this is where spend is cut while
# # planning quality stays with the frontier parent.
# =============================================================================
# Honcho Integration (Cross-Session User Modeling)
# =============================================================================
# AI-native persistent memory via Honcho (https://honcho.dev/).
# Builds a deeper understanding of the user across sessions and tools.
# Runs alongside USER.md β€” additive, not a replacement.
#
# Requires: pip install honcho-ai
# Config: ~/.honcho/config.json (shared with Claude Code, Cursor, etc.)
# API key: HONCHO_API_KEY in ~/.hermes/.env or ~/.honcho/config.json
#
# Hermes-specific overrides (optional β€” most config comes from ~/.honcho/config.json):
# honcho: {}
# =============================================================================
# Display
# =============================================================================
display:
# Use compact banner mode (hides the ASCII-art banner, shows a single line).
# true: Compact single-line banner
# false: Full ASCII banner with tool/skill summary (default)
compact: false
# Tool progress display level (CLI and gateway)
# off: Silent β€” no tool activity shown, just the final response
# new: Show a tool indicator only when the tool changes (skip repeats)
# all: Show every tool call with a short preview (default)
# verbose: Full args, results, and debug logs (same as /verbose)
# log: Silent in chat; append every tool call to ~/.hermes/logs/tool_calls.log (gateway only)
# Toggle at runtime with /verbose in the CLI
tool_progress: all
# Per-platform defaults can be quieter than the global setting. Telegram
# tunes for mobile: tool_progress and busy_ack_detail default off (no
# per-tool breadcrumb stream, no "iteration 21/60" debug detail in busy
# acks or heartbeats), but interim_assistant_messages and
# long_running_notifications STAY ON so the user has real signal between
# turn start and final answer (mid-turn assistant commentary + a single
# edit-in-place "⏳ Working β€” N min" heartbeat). Override under
# display.platforms.telegram.
# Auto-cleanup of temporary progress bubbles after the final response lands.
# On platforms that support message deletion (currently Telegram), this
# removes the tool-progress bubble, "⏳ Still working..." notices, and
# context-pressure status messages once the final reply has been delivered β€”
# keeping long-running turns visible live, then tidy afterward. Failed runs
# leave the bubbles in place as breadcrumbs. Off by default.
# Per-platform override: display.platforms.telegram.cleanup_progress
# true: Delete tracked progress/status bubbles on successful turn
# false: Leave everything in place (default)
# Example:
# display:
# platforms:
# telegram:
# cleanup_progress: true
cleanup_progress: false
# Natural mid-turn assistant updates.
# On gateway platforms, when true, completed assistant status messages are
# sent as separate chat messages. On the Desktop app, when true, mid-turn
# assistant narration streamed between tool calls is kept in the transcript
# instead of the bubble collapsing to only the final message on completion.
# Independent of tool_progress and gateway streaming.
# true: Keep/send mid-turn assistant updates (default)
# false: Only keep/send the final response
interim_assistant_messages: true
# Gateway-only long-running status heartbeats.
# When false, the platform does not receive periodic "⏳ Working β€” N min"
# notifications even if agent.gateway_notify_interval is non-zero. The
# heartbeat edits a single message in place (where the adapter supports
# editing) instead of posting a new bubble each interval.
# Default: true everywhere, including Telegram (silent agents are worse
# than a single edit-in-place heartbeat).
long_running_notifications: true
# Include detailed iteration/tool/status context in busy acknowledgments
# and long-running heartbeats. When true, busy acks show "iteration 21/60,
# terminal, 10 min" and the heartbeat shows "⏳ Working β€” 12 min,
# iteration 21/60, terminal". When false (Telegram default), both stay
# terse: "Interrupting current task" and "⏳ Working β€” 12 min, terminal".
busy_ack_detail: true
# What Enter does when Hermes is already busy (CLI and gateway platforms).
# interrupt: Interrupt the current run and redirect Hermes (default)
# queue: Queue your message for the next turn
# steer: Inject your message mid-run via /steer, arriving at the agent
# after the next tool call β€” no interrupt, no role violation.
# Falls back to 'queue' if the agent isn't running yet or if
# images are attached (steer only carries text).
# Ctrl+C (or /stop in gateway) always interrupts regardless of this setting.
# Toggle at runtime with /busy <interrupt|queue|steer>.
busy_input_mode: interrupt
# Background process notifications (gateway/messaging only).
# Controls how chatty the process watcher is when you use
# terminal(background=true, notify_on_complete=true) from Telegram/Discord/etc.
# concise: One-line status message on completion; failures include a short
# output tail (default)
# off: No watcher messages at all
# result: Only the final raw-output completion message
# error: Only the final raw-output message when exit code != 0
# all: Running output updates + final raw-output message
background_process_notifications: concise
# Play terminal bell when agent finishes a response.
# Useful for long-running tasks β€” your terminal will ding when the agent is done.
# Works over SSH. Most terminals can be configured to flash the taskbar or play a sound.
# true: Ring the terminal bell on each response
# false: Silent (default)
bell_on_complete: false
# Show model reasoning/thinking before each response.
# When enabled, a dim box shows the model's thought process above the response.
# Toggle at runtime with /reasoning show or /reasoning hide.
# true: Show the reasoning box
# false: Hide reasoning (default)
show_reasoning: false
# Stream tokens to the terminal as they arrive instead of waiting for the
# full response. The response box opens on first token and text appears
# line-by-line. Tool calls are still captured silently.
# true: Stream tokens as they arrive (default)
# false: Wait for the full response before rendering
streaming: true
# Show [HH:MM] timestamps on user input and assistant response labels.
# timestamps: false
# ───────────────────────────────────────────────────────────────────────────
# Skin / Theme
# ───────────────────────────────────────────────────────────────────────────
# Customize CLI visual appearance β€” banner colors, spinner faces, tool prefix,
# response box label, and branding text. Change at runtime with /skin <name>.
#
# Built-in skins:
# default β€” Classic Hermes gold/kawaii
# ares β€” Crimson/bronze war-god theme with spinner wings
# mono β€” Clean grayscale monochrome
# slate β€” Cool blue developer-focused
# daylight β€” Bright light-mode theme
# warm-lightmode β€” Warm paper-tone light-mode theme
# poseidon β€” Sea-green/teal Olympian theme
# sisyphus β€” Earthy stone-and-moss theme
# charizard β€” Fiery orange dragon theme
#
# Custom skins: drop a YAML file in ~/.hermes/skins/<name>.yaml
# Schema (all fields optional, missing values inherit from default):
#
# name: my-theme
# description: Short description
# colors:
# banner_border: "#HEX" # Panel border
# banner_title: "#HEX" # Panel title
# banner_accent: "#HEX" # Section headers (Available Tools, etc.)
# banner_dim: "#HEX" # Dim/muted text
# banner_text: "#HEX" # Body text (tool names, skill names)
# ui_accent: "#HEX" # UI accent color
# response_border: "#HEX" # Response box border color
# spinner:
# waiting_faces: ["(βš”)", "(⛨)"] # Faces shown while waiting
# thinking_faces: ["(βš”)", "(⌁)"] # Faces shown while thinking
# thinking_verbs: ["forging", "plotting"] # Verbs for spinner messages
# wings: # Optional left/right spinner decorations
# - ["βŸͺβš”", "βš”βŸ«"]
# - ["βŸͺβ–²", "β–²βŸ«"]
# branding:
# agent_name: "My Agent" # Banner title and branding
# welcome: "Welcome message" # Shown at CLI startup
# response_label: " βš” Agent " # Response box header label
# prompt_symbol: "βš”" # Prompt symbol (bare token; renderers add trailing space)
# tool_prefix: "β•Ž" # Tool output line prefix (default: β”Š)
#
skin: default
# =============================================================================
# Model Aliases β€” short names for /model command
# =============================================================================
# Map short aliases to exact (model, provider, base_url) tuples.
# Used by /model tab completion and resolve_alias().
# Aliases are checked BEFORE the models.dev catalog, so they can route
# to endpoints not in the catalog (e.g. Ollama Cloud, local servers).
#
# model_aliases:
# opus:
# model: claude-opus-4-6
# provider: anthropic
# qwen:
# model: "qwen3.5:397b"
# provider: custom
# base_url: "https://ollama.com/v1"
# glm:
# model: glm-4.7
# provider: custom
# base_url: "https://ollama.com/v1"
# =============================================================================
# Privacy
# =============================================================================
# privacy:
# # Redact PII from the LLM context prompt.
# # When true, phone numbers are stripped and user/chat IDs are replaced
# # with deterministic hashes before being sent to the model.
# # Names and usernames are NOT affected (user-chosen, publicly visible).
# # Routing/delivery still uses the original values internally.
# redact_pii: false
# =============================================================================
# Shell-script hooks
# =============================================================================
# Register shell scripts as plugin-hook callbacks. Each entry is executed as
# a subprocess (shell=False, shlex.split) with a JSON payload on stdin. On
# stdout the script may return JSON that either blocks the tool call or
# injects context into the next LLM call.
#
# Valid events (mirror hermes_cli.plugins.VALID_HOOKS):
# pre_tool_call, post_tool_call, pre_llm_call, post_llm_call,
# pre_api_request, post_api_request, on_session_start, on_session_end,
# on_session_finalize, on_session_reset, subagent_stop
#
# First-use consent: each (event, command) pair prompts once on a TTY, then
# is persisted to ~/.hermes/shell-hooks-allowlist.json. Non-interactive
# runs (gateway, cron) need --accept-hooks, HERMES_ACCEPT_HOOKS=1, or the
# hooks_auto_accept key below.
#
# See website/docs/user-guide/features/hooks.md for the full JSON wire
# protocol and worked examples.
#
# hooks:
# pre_tool_call:
# - matcher: "terminal"
# command: "~/.hermes/agent-hooks/block-rm-rf.sh"
# timeout: 10
# post_tool_call:
# - matcher: "write_file|patch"
# command: "~/.hermes/agent-hooks/auto-format.sh"
# pre_llm_call:
# - command: "~/.hermes/agent-hooks/inject-cwd-context.sh"
# subagent_stop:
# - command: "~/.hermes/agent-hooks/log-orchestration.sh"
#
# hooks_auto_accept: false
# =============================================================================
# Telemetry
# =============================================================================
# Shared metrics are disabled by default. When enabled, Hermes writes only
# allowlisted aggregate counters and immutable JSON
# packages under $HERMES_HOME/telemetry/shared_metrics; it does not upload them.
# Packages include a random profile-scoped ID that stays stable until this
# directory is deleted. It is not derived from hardware, account, or host data.
# Successfully exported local history is retained for 30 days; pending deltas
# are retained until they can be exported.
# This profile-owned choice is not overridden by managed-scope configuration.
telemetry:
shared_metrics:
enabled: false
# =============================================================================
# Update Behavior
# =============================================================================
updates:
# Create a full HERMES_HOME zip before every `hermes update`.
# Backups land in ~/.hermes/backups/ and can be restored with `hermes import`.
# Off by default because large homes can add minutes to every update.
pre_update_backup: false
# Number of pre-update backup zips to retain.
backup_keep: 5
# What non-interactive updates do with local source edits in the Hermes repo.
# Interactive terminal updates always prompt before restoring the autostash.
#
# stash - auto-stash before pull, then auto-restore after success (default)
# discard - drop the update-created stash after success; use only on managed
# installs where local source edits should not persist
non_interactive_local_changes: "stash"
# =============================================================================
# Web Dashboard
# =============================================================================
# OAuth gate configuration for `hermes dashboard --host <non-loopback>`.
# The bundled Nous Portal plugin reads these on startup; settings here are
# the canonical surface. Each can be overridden by an environment variable:
#
# dashboard.oauth.client_id <- HERMES_DASHBOARD_OAUTH_CLIENT_ID
# dashboard.oauth.portal_url <- HERMES_DASHBOARD_PORTAL_URL
# dashboard.public_url <- HERMES_DASHBOARD_PUBLIC_URL
#
# Env wins when set to a non-empty value. This is what Fly.io's platform-
# secret injection uses to push per-deploy client_ids without needing to
# bake a config.yaml into the image. Empty env values are treated as unset
# so a provisioned-but-not-populated secret can't shadow a valid entry here.
#
# Local dev / on-prem deploys should typically set these via config.yaml
# (the ~/.hermes/.env file is reserved for API keys and secrets).
#
# dashboard:
# oauth:
# client_id: "" # agent:{instance_id}; Portal provisions this at deploy
# portal_url: "" # blank β†’ default https://portal.nousresearch.com
#
# # Force the absolute base URL the OAuth callback (and any other public
# # URL the dashboard hands to external systems) is built from. Set this
# # for deploys behind reverse proxies that don't reliably forward
# # X-Forwarded-Host / X-Forwarded-Proto / X-Forwarded-Prefix (manual
# # nginx setups, on-prem ingresses, custom-domain Fly deploys without
# # full proxy header chains).
# #
# # When set, the value is the complete authority: scheme + host +
# # optional path prefix (e.g. "https://example.com/hermes"). The OAuth
# # callback URL becomes "<public_url>/auth/callback" β€” X-Forwarded-Prefix
# # is IGNORED on this code path because the operator has explicitly
# # declared the public URL and we no longer need to guess.
# #
# # Leave empty to use the existing proxy-header reconstruction (the
# # default β€” works on Fly.io out of the box).
# #
# # public_url: "https://example.com/hermes"
#
# -----------------------------------------------------------------------------
# Self-hosted OIDC dashboard auth (generic OpenID Connect β€” Authentik,
# Keycloak, Zitadel, Authelia, Auth0, Okta, Google, …). Use this INSTEAD of the
# nous block above when gating the dashboard with your own identity provider.
# Each setting can be overridden by an environment variable:
#
# dashboard.oauth.self_hosted.issuer <- HERMES_DASHBOARD_OIDC_ISSUER
# dashboard.oauth.self_hosted.client_id <- HERMES_DASHBOARD_OIDC_CLIENT_ID
# dashboard.oauth.self_hosted.scopes <- HERMES_DASHBOARD_OIDC_SCOPES
# dashboard.oauth.self_hosted.client_secret <- HERMES_DASHBOARD_OIDC_CLIENT_SECRET
#
# dashboard:
# oauth:
# provider: self-hosted
# self_hosted:
# issuer: "https://auth.example.com/application/o/hermes/" # required
# client_id: "hermes-dashboard" # required
# scopes: "openid profile email" # optional
#
# # OPTIONAL β€” set ONLY if your IDP registered the client as
# # *confidential* (Authentik / Keycloak often default to this). When
# # set, Hermes authenticates the client at the token endpoint
# # (client_secret_basic or client_secret_post, auto-selected from the
# # IDP's discovery doc) IN ADDITION to PKCE. Leave unset for a public
# # (PKCE-only) client β€” the common case.
# #
# # This is a CREDENTIAL: prefer setting HERMES_DASHBOARD_OIDC_CLIENT_SECRET
# # in ~/.hermes/.env over putting it here in config.yaml.
# # client_secret: ""
# =============================================================================
# External secret sources
# =============================================================================
# Pull provider credentials from external secret managers at process startup
# instead of storing them in ~/.hermes/.env. Only the manager's bootstrap
# credential (e.g. BWS_ACCESS_TOKEN / OP_SERVICE_ACCOUNT_TOKEN) lives in .env
# (or your shell / desktop session); everything else rotates centrally.
# Failures never block startup β€” Hermes warns once and continues with
# whatever .env already had.
#
# Multiple sources can be enabled at once:
# - "mapped" sources (explicit VAR -> ref bindings, e.g. 1Password's env:
# map) beat "bulk" sources (whole-project dumps like Bitwarden BSM)
# - within a shape, the first source to claim a var wins; later claims
# are skipped with a startup warning (never a silent clobber)
# - a source's override_existing lets it beat .env/shell values, but
# never another secret source's claim
# Docs: https://hermes-agent.nousresearch.com/docs/user-guide/secrets/
#
# secrets:
# # Optional explicit ordering of enabled sources.
# # sources: [onepassword, bitwarden]
#
# # ---- Bitwarden Secrets Manager (bws CLI) --------------------------------
# bitwarden:
# enabled: false
# access_token_env: BWS_ACCESS_TOKEN # bootstrap token, sourced from .env
# project_id: "" # UUID of the BSM project to sync
# server_url: "" # "" = US Cloud; EU/self-hosted URL otherwise
# cache_ttl_seconds: 300 # 0 disables fresh caching
# encrypted_cache: # optional encrypted stale fallback
# enabled: false
# max_stale_seconds: 0 # 0 disables stale fallback
# override_existing: true # BSM values win over existing env
# auto_install: true # lazy-download bws into ~/.hermes/bin
#
# # ---- 1Password (op CLI) -------------------------------------------------
# onepassword:
# enabled: false
# # Map env-var names to op:// secret references. Each is resolved with a
# # single `op read` at startup.
# env:
# OPENAI_API_KEY: "op://Private/OpenAI/api key"
# ANTHROPIC_API_KEY: "op://Private/Anthropic/credential"
# account: "" # op --account shorthand; "" = default
# service_account_token_env: OP_SERVICE_ACCOUNT_TOKEN # headless auth; unset = desktop session
# binary_path: "" # "" = resolve op via PATH; else absolute path
# cache_ttl_seconds: 300 # 0 disables BOTH cache layers
# override_existing: true # resolved values win over existing env
#
# # ---- Command helper (any CLI vault) --------------------------------------
# # Run a user-configured helper that prints KEY=VALUE lines on stdout β€”
# # works with any secret store that has a CLI: keepassxc-cli, secret-tool,
# # pass, gpg, or a script that cats a tmpfs env file. Composes with the
# # sources above (enable any combination). POSIX-only (needs /bin/sh).
# # The helper must be fast and NON-interactive (hard timeout, 1 MiB cap);
# # its stderr is discarded so diagnostics can't leak secret material.
# command:
# enabled: false
# command: "cat /run/user/1000/hermes-secrets.env"
# helper_timeout_seconds: 3
# override_existing: false # .env/shell win by default
# # This runs ONCE per process at startup, so it cannot replace a credential
# # that expires mid-session. For a single provider whose token needs
# # re-minting during a session, use `providers.<name>.key_cmd` instead.