diff --git "a/cli-config.yaml.example" "b/cli-config.yaml.example" new file mode 100644--- /dev/null +++ "b/cli-config.yaml.example" @@ -0,0 +1,2138 @@ +# Hermes Agent CLI Configuration +# Copy settings from this example into ~/.hermes/config.yaml, or use +# `hermes config set ` 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 + # + # Durability level for every state.db connection: OFF, NORMAL, FULL, EXTRA + # (or 0-3). Unset leaves SQLite's default, which is baked in at compile time + # (SQLITE_DEFAULT_WAL_SYNCHRONOUS) and therefore differs between the bundled + # interpreter, a distro python3 and a Homebrew one. Set it if you need to + # know which one you are running rather than infer it. On macOS this is a + # floor, not a pin: values below FULL are refused because Darwin's fsync() + # does not guarantee write ordering, while EXTRA is honored normally. + # synchronous: FULL + +# ============================================================================= +# 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" + + # Stream API responses from the provider (default: true). The agent core + # prefers streaming for every turn — subagents included — for liveness + # health-checking. Set false to force non-streaming requests for the whole + # session (persists across mid-session model switches). Escape hatch for + # self-hosted OpenAI-compatible servers whose streaming tool-call path is + # broken (e.g. vLLM with --tool-call-parser qwen3_xml + a reasoning parser + # can leak tool calls into plain text instead of returning tool_calls — + # #72901). Orthogonal to display.streaming, which controls token rendering + # only. + # streaming: true + + # Azure Foundry keyless auth example: + # provider: "azure-foundry" + # base_url: "https://.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 + # + # Output-token limits are provider-owned, not user configuration. Native + # protocols requiring a limit receive an internal value from Hermes. + + # ── 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) +# providers: +# router: +# base_url: https://api.router.com/v1 +# api_key: ${RAMP_ROUTER_API_KEY} +# # api_mode auto-detected as codex_responses for api.router.com — the +# # Responses API is Router's native wire (chat/completions is only a +# # compatibility shim). The bundled router provider covers this; a named +# # custom provider is only needed for a non-default Router-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://.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://.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://.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" +# +# # Per-model overrides: same keys, applied only when the agent is on that model +# # (spelling-tolerant match; unset keys fall through to the flat values above). +# # models: +# # "openai/gpt-6-astra": +# # only: ["openai"] +# # "anthropic/claude-fable-5.1": +# # only: ["anthropic"] + +# ============================================================================= +# 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 +cron: + catch_up_missed: true # False skips past-grace recurring misses after planned downtime. + +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 + # Let an authenticated browser extension register as the controller for an + # existing Hermes session. Disabled by default. Local API registration also + # requires the API server bearer key to be configured. + extension_control: + enabled: false + + # Maximum characters of snapshot content before truncate-and-store. + # Increase for long pages (for example, documentation or financial reports), + # or decrease to reduce context usage. Minimum: 1000; default: 15000 + # (same per-page budget as web_extract). + # snapshot_threshold: 15000 + +# ============================================================================= +# 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 stay +# opt-in for interactive CLI/TUI/Desktop/ACP sessions, but default on for unattended +# gateway/cron sessions where nobody is present to interrupt a model that +# ignores loop warnings. +tool_loop_guardrails: + warnings_enabled: true + hard_stop_enabled: false + non_interactive_hard_stop_enabled: true + 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 + + # Fail closed before lossy compaction unless an active memory provider that + # implements the pre-compress checkpoint contract (API v2) confirms its + # durable checkpoint (default: false). With this on and no checkpoint, the + # compaction attempt errors with BLOCKED_MISSING_PREREQUISITE and the + # uncompressed transcript is preserved for a later retry. The gate binds to + # every compaction authority: server-side native compaction + # (codex_responses_native) is suppressed while armed, post-turn + # micro-compaction is forced off (no checkpoint hook in its path), and the + # codex_app_server API mode is refused at agent init (the codex agent + # compacts its own thread — no checkpoint can be guaranteed there). Only + # enable it with a checkpoint-capable provider configured — see + # website/docs/developer-guide/memory-provider-plugin.md + # ("Pre-Compress Checkpoints"). + checkpoint_required: false + + # 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 + + # Optional absolute server compaction trigger in input tokens. The default + # null value follows the resolved local compression trigger with an 8192 token + # safety margin. For example, a local trigger of 765000 selects 756808. + # A positive integer stays absolute and only clamps downward when needed so + # the server compacts first. Invalid values use this automatic behavior. If + # the local trigger is unavailable, automatic mode uses 200000. + codex_responses_compact_threshold: null + + # 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 + + # Gateway session-hygiene turn-hold budget (default: 10). Max seconds an + # arriving user turn is held while a still-streaming hygiene summary + # finishes. Distinct from hygiene_timeout_seconds (compressor inactivity + # budget): this bounds user-visible latency so chat transports (Telegram + # ~30s) do not drop a silent connection. On expiry the turn proceeds + # uncompressed; the detached worker keeps its commit admission (when the + # commit is watermark-fenced) and the summary is adopted at the next safe + # boundary. Thinking-model summarizers often need longer than 10s to emit + # the first content token — raise to 300 (or >= your summarizer's real + # time-to-first-content) only if you want THIS turn to wait for the + # compression instead of adopting it one turn late. + hygiene_max_turn_hold_seconds: 10 + + # 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). + +# ============================================================================= +# Tool-result budget (optional) +# ============================================================================= +# Controls when a large tool result is spilled to disk (full output saved to +# $HERMES_HOME/cache/spillover, preview + path kept in context). MCP tool +# results (tools named mcp_*) spill at a tighter 50,000-char threshold than +# the generic 100K default: MCP servers routinely return un-paginated 20-50K +# payloads that bloat context and slow every subsequent turn. Nothing is +# lost — the full result is on disk and readable with read_file. +# +# tool_budget: +# mcp_result_size_chars: 50000 # per-result spillover threshold for mcp_* tools + +# ============================================================================= +# 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) + +# ============================================================================= +# Session Continuity (Messaging Platforms) +# ============================================================================= +# Conversations persist across inactivity and daily boundaries. Use /new or +# /reset explicitly; context compression manages long conversations. + +# 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 + +# ============================================================================= +# Session Storage Retention (state.db) +# ============================================================================= +# ~/.hermes/state.db keeps every session, message, and tool call, plus the +# FTS5 search indexes. Since #54189, auto-pruning is ON by default so the file +# stays bounded: at CLI/gateway/cron startup (at most once per +# min_interval_hours) Hermes deletes ENDED sessions whose last activity is +# older than retention_days. Open, pinned, and in-progress sessions are never +# deleted. Stale automation sessions (cron/kanban/subagent/one-shot CLI) whose +# process died without closing them are first *closed*, then aged through a +# further full retention window before removal. +# +# After a prune that removed rows, VACUUM reclaims disk space only when both +# the time throttle (min_vacuum_interval_days) has elapsed AND more than 25% +# of the file's pages are reclaimable — a dense database never pays for a full +# rewrite to reclaim a few MB. +# +# Uncomment to change the defaults shown; set auto_prune: false to keep every +# ended session forever (the pre-#54189 behavior). +# sessions: +# auto_prune: true +# retention_days: 90 +# vacuum_after_prune: true +# min_vacuum_interval_days: 30 +# min_interval_hours: 24 + +# Optional direct endpoint for autonomous Bot Mode rooms spanning gateways. +# Leave unset for the safe default: Desktop coordinates cross-gateway rooms and +# same-gateway rooms can still continue on their own. Set this only to the +# public HTTPS base URL that another trusted Hermes gateway can reach. The API +# server key authorizes the initial invitation; scoped grants are signed by a +# separate installation-private secret and survive API-key rotation. Disbanding +# a Group Chat revokes its known peer routes; API-key rotation alone does not. +# Network access should still be limited to the peer gateways (for example with +# your VPN or firewall). +# Plain HTTP is accepted only for loopback testing; every reachable peer URL +# must use HTTPS. +# +# gateway: +# room_link_url: "https://hermes.example.net" +# +# The equivalent environment override is HERMES_ROOM_LINK_URL. After changing +# it, restart the gateway and reopen New Group Chat > Advanced; Hermes will +# re-check the selected gateways automatically. + +# Startup sweep of session rows orphaned by a dead gateway process. +# The normal disconnect cleanup runs on an in-process grace timer, so a +# gateway restart (update, crash, systemd) leaves those rows permanently +# "active". On every gateway boot — stdio TUI *and* the desktop/dashboard +# WS sidecar — tui/desktop/subagent rows whose start time AND newest +# message are both older than the session TTL (HERMES_TUI_SESSION_TTL_S, +# default 6h) are closed with end_reason "startup_orphan_reap". +# Messaging-platform sessions (Telegram, Discord, ...) are never touched; +# live in-memory sessions are excluded; swept sessions stay resumable. +# +# dashboard: +# startup_orphan_sweep: 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 5-second default. + # gateway_turn_lease_timeout: 5 + + # 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 + + # Transcript-sanitiser repeated-heal escalation (#96870). The pre-send + # sanitiser silently repairs empty non-final turns on the wire copy; after + # this many heal passes within a 10-minute session window Hermes logs one + # ERROR (with session id + heal pattern) and sends a ONE-TIME out-of-band + # notice to the user suggesting /debug share or `hermes doctor`. The notice + # goes through the status channel only — conversation context and prompt + # caching are untouched. Set 0 to disable escalation (per-window WARNINGs + # still fire). Default 3. + # sanitizer_heal_escalation_threshold: 3 + + # 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: {} + + # Fast mode (OpenAI Priority Processing / xAI Grok 4.6 / Anthropic Fast Mode + # on Opus 4.8+). Premium pricing; only sent to first-party endpoints. + # "" / "normal" - off (default) + # "fast" - every request + # "auto" - only the first fast_auto_seconds of every turn + # "cold" - that window on the first turn of a session only + # Also: /fast normal|fast|auto|cold [--global] + service_tier: "" + fast_auto_seconds: 60 + + # 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" + +# ============================================================================= +# Gateway Lifecycle +# ============================================================================= +gateway: + # Post-interrupt grace for running agents during an unexpected SIGTERM. + # Adapter, bridge, and database teardown starts after this many seconds even + # if an agent has not unwound. Keep it below the service-manager stop budget. + # signal_interrupt_grace_timeout: 1 + + # Let platform adapters honor HTTP_PROXY / HTTPS_PROXY / NO_PROXY (and + # SSL_CERT_FILE) from the process environment, plus macOS system-proxy + # auto-detection. Set to false when the gateway inherits a proxy it must not + # use (e.g. a Windows Scheduled Task picking up a local Clash/V2Ray proxy that + # isn't running). Explicit per-platform vars like DISCORD_PROXY still apply. + # trust_env: true + +# ============================================================================= +# 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 is applied across core + plugin + skill commands before +# # the cap, so a listed skill command always keeps a menu slot. +# priority: +# - my_plugin_command +# - my-important-skill +# 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 +# # Slack user IDs whose Web-API posts (user token, e.g. your own +# # dashboard/mobile front-end) count as human instead of being dropped +# # as app traffic. Narrower than allow_bots: all. Users only — never apps. +# api_human_users: ["U0AAAAAAA", "U0BBBBBBB"] +# # Suppress automatic link-preview cards without removing clickable links. +# # Omit either key to preserve Slack's default for that preview type. +# unfurl_links: false +# unfurl_media: 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.`` +# 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) + # Local execution uses a persistent session kernel: variables, imports, and + # loaded data survive across execute_code calls in one conversation, so + # multi-step data work stops re-loading its inputs every call. Pass + # reset=true on a call to discard state; a timed-out/interrupted cell kills + # the kernel (next call starts fresh). Remote terminal backends currently + # run per-call (remote kernel host is tracked follow-up work). Security + # scrubbing, tool whitelist, and output redaction are identical either way. + # kernel_idle_timeout: 1800 # Reap kernels idle longer than this (seconds) + # max_session_kernels: 4 # Process-wide LRU cap on live kernels + +# ============================================================================= +# 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. + # fallback_providers: # Fallback chain for delegated children (same entry format as the + # - provider: "openrouter" # top-level list). Pinned children use only a declared child chain; + # model: "deepseek/deepseek-chat" # unpinned children inherit when unset. [] disables fallback (#65038). + +# ============================================================================= +# 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 . + 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 + + # Play terminal bell when a blocking prompt opens and waits on you: + # clarify questions, dangerous-command approvals, sudo password, secret + # capture. Same mechanism as bell_on_complete (\a) — works over SSH. + # true: Ring whenever the agent is waiting for your input + # false: Silent (default) + bell_on_prompt: 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 . + # + # 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/.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). +# +# An alias pointing at its own endpoint can carry that endpoint's +# credential with `api_key` (a literal, or a "${VAR}" reference) or +# `key_env` (an env var name). `api_key` wins if both are set. When +# neither is set the key is resolved from the alias HOST — never from +# whatever provider was active before the switch, which would send that +# provider's secret to an unrelated third party. +# +# 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" +# theta: +# model: theta-1 +# provider: custom +# base_url: "https://theta.example.com/v1" +# key_env: THETA_API_KEY # or: api_key: "${THETA_API_KEY}" + +# ============================================================================= +# 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. +# 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. +# +# Nothing is uploaded unless you also set `send: true`. That is a separate +# opt-in and requires `enabled`; it never turns collection on by itself. +# When sending is on: +# * only packages whose entire collection period falls within one +# continuous recorded consent window are ever sent. Consent windows open +# when you enable sending and close when you disable it, so data +# collected before you opted in — or during any gap between opt-ins — +# stays on this machine; +# * each package carries the profile-scoped ID as-is. It is a random UUID +# with no hardware, account, or host-derived content, and deleting the +# shared-metrics directory resets it. +# See docs/observability/relay-shared-metrics.md (Appendix A) for the full +# consent, identity, retention, and deletion decisions. +telemetry: + shared_metrics: + enabled: false + send: false + # endpoint: https://telemetry.nousresearch.com/v1/telemetry + + +# ============================================================================= +# Update Behavior +# ============================================================================= +updates: + # Disable passive CLI/banner update checks with `hermes config set updates.check false`. + # Explicit `hermes update --check` and `hermes update` remain available. + check: true + # 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 `. +# 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 "/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" +# # +# # Reverse proxies connecting from another container or host are not +# # trusted by default. Add only the proxy's exact IP address, or a bounded +# # CIDR for a dedicated proxy network, so X-Forwarded-Proto and +# # X-Forwarded-For can be honored. Loopback is always trusted. Wildcards +# # and /0 networks are rejected. +# # +# # trusted_proxies: +# # - "172.20.0.5" +# # # - "172.20.0.0/24" # dedicated network, if the IP is dynamic +# +# ----------------------------------------------------------------------------- +# 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..key_cmd` instead.