Files
hermes-agent/hermes_cli/config_defaults.py
Brooklyn Nicholson ea647bd518 fix(desktop): expose the renderer accessibility tree to dictation/IME tools (#118271)
Chromium only builds the renderer's accessibility tree when it detects
an assistive technology — and dictation tools that insert text through
the OS accessibility APIs (Wispr Flow etc.) do not register the way a
screen reader does, so the tree never appeared: the window exposed only
its chrome and the contenteditable composer was unreachable. No
setAccessibilitySupportEnabled call existed anywhere in the electron
tree, on any platform.

- apps/desktop/electron/renderer-accessibility.ts (new): the
  platform/env decision in a dependency-free module (app object
  injected) — ON by default on darwin/win32 where the typed API exists,
  opt-out via HERMES_DESKTOP_RENDERER_ACCESSIBILITY=0. Linux is untouched
  (ATK flow needs no manual enable; the API is not typed there).
- main.ts calls it right after app ready (the API's requirement).
- hermes desktop launcher: `desktop.renderer_accessibility: false` in
  config.yaml bridges to the env var — only the opt-out is bridged, so
  the default never depends on the launcher path. Same shape as
  desktop.disable_gpu. config_defaults documents the key.

Family note: the same seam on Windows is #92607 (Wispr Flow there);
this one enablement should resolve both platforms, but the Windows leg
is unverified on this machine, so #92607 stays open. The dictation
keybind half of #118271 already landed separately in 3cc24b0130
(composer.dictate action; rebindable-key ask tracked by #112118/#71658,
left alone).

Tests: 4 electron-side unit tests (default-on per platform, Linux skip,
opt-out words + explicit env precedence, injected app switch flips
exactly when enabled); Python side: 12-case normalization parametrize +
env-bridging precedence test (ON sets nothing, opt-out bridges, explicit
env wins) in test_gui_command.py; _desktop_launch_options signature
consumers updated. All green; the remaining lucide-react TS2307 in
onboarding setup.tsx is pre-existing on base (missing dep of that
workspace file, untouched here).

Fixes #118271
2026-09-26 21:57:37 -05:00

3144 lines
207 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Default configuration data for Hermes Agent: DEFAULT_CONFIG and OPTIONAL_ENV_VARS.
Pure-data leaf module — must not import from hermes_cli.config. Comments are the user-facing
docs of config.yaml.
"""
def _aux(timeout, *, reasoning_effort=True, **extra):
"""Standard auxiliary-task model block (see DEFAULT_CONFIG["auxiliary"]).
reasoning_effort=False omits that key (MoA blocks configure depth per slot);
``extra`` keys are appended after the standard ones.
"""
d = {"provider": "auto", "model": "", "base_url": "", "api_key": "", "timeout": timeout, "extra_body": {}}
if reasoning_effort:
d["reasoning_effort"] = ""
d.update(extra)
return d
DEFAULT_CONFIG = {
"model": "",
"providers": {},
"fallback_providers": [],
# min_switch_reset_seconds: opt-in (0 = off). When a rate-limited primary declares a reset
# sooner than this many seconds, stay on it (the retry backoff rides out the window) instead
# of switching the turn to a fallback model.
"fallback": {"min_switch_reset_seconds": 0},
"credential_pool_strategies": {},
"toolsets": ["hermes-cli"],
# journal_mode: SQLite journal mode for every Hermes DB. "wal" default; use "delete" on
# weak-fsync/shared filesystems where WAL is not crash-safe (macOS virtiofs, NFS, SMB).
"database": {
"journal_mode": "wal",
# WAL sizing pragmas (ints). None = SQLite defaults (autocheckpoint 1000 pages, no limit).
"wal_autocheckpoint": None,
"journal_size_limit": None,
},
# Soft fd limit for long-running server processes; clamped to OS hard limit. 0/false/null = off.
"runtime": {"nofile_soft_limit": 4096},
# Global active chat session cap across CLI, TUI/dashboard, and messaging. None/0 = unbounded.
"max_concurrent_sessions": None,
# Soft LRU cap on in-memory TUI/desktop/dashboard sessions. Above it the gateway evicts the
# least-recently-active DETACHED sessions (no live client); reopening re-resumes from disk.
# 0/null disables.
"max_live_sessions": 16,
"session": {
# Per-terminal `hermes -c`: each CLI session writes a breadcrumb under
# $HERMES_HOME/terminal-sessions/<terminal-id>, so bare -c/--continue resumes THIS
# terminal's session (tmux/kitty/wezterm pane, tty). false = resume globally most-recent.
"terminal_continue": True,
},
"agent": {
# Turn cap. null = unlimited (default; caps caused silent mid-task truncation). Positive int
# caps; "none"/"unlimited"/"inf"/0/-1 also mean unlimited (resolve_turn_limit).
"max_turns": None,
# Optional one-time model-visible checkpoint warning before a finite turn cap is exhausted.
# null = off; set a ratio strictly between 0 and 1 (for example, 0.75).
"budget_warning_ratio": None,
# Wall-clock budget (seconds) per run. null = off. When set: one-time wrap-up notice at 80%
# elapsed; implicit provider stale timeouts capped to remaining budget. CLI equivalent:
# `hermes chat --run-budget N`.
"run_budget_seconds": None,
# Gateway inactivity timeout (seconds). Only fires when the agent is completely idle — not
# while calling tools or receiving API responses. 0 = unlimited.
"gateway_timeout": 1800,
# Max seconds an alias routing key waits for the active turn holding the same session lease;
# on expiry the message is rejected with a resend notice. Keep short: Telegram dispatches
# sequentially, so a waiter delays unrelated topics. Non-positive -> 5s.
"gateway_turn_lease_timeout": 5,
# Per-session AIAgent cache in the gateway. Each entry keeps a warm prompt prefix AND the
# full transcript: too small re-pays uncached prompts, too large fills the heap.
"agent_cache": {
"max_size": 128, # LRU entry cap
"idle_ttl_secs": 3600, # evict agents idle this long
# Anonymous-RSS budget (MB) above which LRU transcripts are shed (reloaded from disk
# next turn). "auto" = derive from cgroup memory limit (or total RAM); number =
# explicit; 0/off = disable the pass.
"memory_high_mb": "auto",
# Max sessions shed per pass (teardown bursts can't stall the gateway) and the number of
# most-recently-used sessions the pass never touches.
"max_evictions_per_pass": 16,
"protect_recent": 8,
},
# Force-interrupt budget (seconds) once gateway stop()/drain has begun (SIGTERM, and the
# final phase of in-band restart). 0 = interrupt immediately. Keep under systemd
# TimeoutStopSec or risk SIGKILL mid-cleanup; for /restart prefer restart_after_turn_timeout
# so turns finish BEFORE stop().
"restart_drain_timeout": 0,
# Cron-only floor under the stop()/drain wait (seconds). Interrupted chat turns resume on
# the next message, but an interrupted cron run is recorded as a permanent failure, so it
# must not inherit restart_drain_timeout's 0. Clamped to the shutdown-watchdog leash minus
# teardown headroom (~50s unless TimeoutStopSec is raised). 0 = opt out.
# A chat turn interrupted by a restart is announced to the user and resumed on their next message;
# an interrupted cron run is written to jobs.json as a permanent failure that nobody is waiting on,
# so it must not inherit restart_drain_timeout's 0 (#82161).
"cron_drain_timeout": 30,
# In-band restart (/restart, SIGUSR1): refuse new work, then wait up to this many seconds
# for in-flight agents/cron/api runs to finish before stop(). 0 = enter stop() at once. 30
# min is a safety valve for wedged agents, not a target; raise for long unattended turns.
# Default 30 min is a safety valve for wedged agents, not a target latency — an interactive `hermes
# gateway restart` must never block for hours on a turn that wedged (#79133).
"restart_after_turn_timeout": 1800,
# Max seconds a submitted prompt waits for the deferred agent build (MCP discovery, model
# metadata, skills scan) before failing visibly. The prompt is delivered as soon as the
# build completes (progress notice past 30s), so this only fires on a hung build. Raise for
# many slow/unreachable MCP servers.
# See #63078.
"build_wait_timeout": 600,
# Hermes-level retry attempts for API errors (connection drops, timeouts, 5xx) wrapping the
# whole call; the OpenAI SDK also retries transient errors (max_retries=2). Set 1 for fast
# failover to fallback providers; raise to tolerate longer provider hiccups.
"api_max_retries": 3,
# Once api_max_retries AND the fallback chain are spent on a transient outage (5xx,
# overloaded/529, connect/read timeouts) with nothing delivered yet, wait and retry this many
# more cycles (jittered 15/30/60/60/60s; a provider Retry-After wins up to 120s) with a
# visible "retrying automatically" countdown instead of ending the turn. Esc/interrupt stops
# the wait; auth/format/billing/policy errors never enter. 0 disables.
"auto_recovery_cycles": 5,
# Seconds the Codex/Responses stream may keep reading after its terminal frame so the relay
# finalizer can run. Relays that never close the SSE socket after response.completed would
# otherwise wedge the turn until the idle watchdog discards the already-billed response
# (#103864). 0 skips the drain. Well-behaved endpoints close immediately and never wait this long.
"stream_drain_timeout": 2.0,
# Empty-response retry guard. Empty retries re-send the full input at full price; this stops
# re-billing deterministic empties (unsignaled refusals, zero output tokens) while failing
# open on ambiguous evidence (missing usage, any tokens, model/provider change).
"empty_response_guard": {
"enabled": True, # False = legacy fixed 3 retries unconditionally
# When one empty attempt's estimated input cost >= this USD, the streak's retry budget
# drops from 3 to 1. Unknown pricing / missing usage leaves it untouched.
"cost_threshold_usd": 0.25,
},
# Fast mode: "" / "normal" (off), "fast" (always), "auto" (first fast_auto_seconds of every
# turn), "cold" (first turn of a session only).
"service_tier": "",
"fast_auto_seconds": 60,
# Responses API final-answer length (`text.verbosity`): "" = not sent (provider default),
# or low | medium | high. Responses-family transports only; chat_completions never sends it.
"text_verbosity": "",
# System-prompt guidance telling the model to call tools instead of describing actions.
# "auto" = gpt/codex models; true/false = force for all models; or a list of model-name
# substrings (e.g. ["gpt", "codex", "gemini", "qwen"]).
"tool_use_enforcement": "auto",
# Execution-discipline prompt block (tool persistence, tools for arithmetic/system facts,
# read-back after external writes, count reconciliation, literal identifiers,
# verification-gated completion). Chosen once per session by model name (byte-stable).
# "auto" = gpt/codex/grok/deepseek/kimi/qwen/glm/minimax/mimo/mistral; true/false = force;
# or a list of model-name substrings.
"execution_guidance": "auto",
# When the model narrates an action ("I'll go check the logs...") but emits no tool call,
# inject a "continue now, execute the tools" nudge and loop (max 2 nudges/turn). Corrective
# sibling of tool_use_enforcement. "auto" = codex_responses api_mode only; true = all
# api_modes (fixes Gemini/Claude "stops after stating intent"); false = never; or a list of
# model-name substrings.
"intent_ack_continuation": "auto",
# Anti-stall guards: (1) identical-call loop breaker appends a notice when the same tool is
# called 3+ times with identical args AND results (never blocks; pollers like `process`
# exempt); (2) continue-intent extension of empty-response recovery re-prompts once when the
# model says it will continue but takes no action. False disables both.
"stall_guards": True,
# "Finish the job" prompt block for all models: don't stop at a stub, never fabricate output
# when the real path is blocked. ~80 cached tokens. False disables.
"task_completion_guidance": True,
# Prompt block for all models steering independent tool calls (reads, searches, fetches,
# read-only commands) into one batched turn; the runtime already runs them concurrently. ~70
# cached tokens. False disables.
"parallel_tool_call_guidance": True,
# Toolchain probe: surfaces Python/pip/uv/PEP-668 state in the system prompt only when
# something non-default is detected (no pip module, pip/python mismatch, PEP 668 without
# uv); zero tokens when clean. Skipped for docker/modal/ssh backends (own probe).
"environment_probe": True,
# Bot Mode teammate-messaging protocol section (silent unless desktop Bot Mode manages it).
"bot_mode_protocol": True,
# Embedder-supplied text appended to the system prompt's environment-hints block, so a host
# wrapping Hermes (sandbox runner, managed platform) can describe proxy/credential/ mount
# layout without editing SOUL.md. Env HERMES_ENVIRONMENT_HINT overrides it.
"environment_hint": "",
# Coding posture: on interactive coding surfaces (CLI, TUI, desktop, ACP) in a code
# workspace, add a coding brief + live git/workspace snapshot to the system prompt
# (agent/coding_context.py). "auto" = prompt-only when interactive AND cwd is a code
# workspace (toolsets untouched, messaging platforms unaffected); "focus" = auto + collapse
# toolset to the lean coding set (+ enabled MCP servers) + demote non-coding skill
# categories to names-only (explicit opt-in); "on" = force everywhere; "off" = disable.
"coding_context": "auto",
# Standing operator instructions (string or list) appended to the coding brief as an extra
# stable system block — project-wide workflow rules, e.g. "Don't run tsc/lint until I
# approve." Cache-safe: takes effect next session.
"coding_instructions": "",
# When verify-on-stop finds edits without fresh verification evidence, add guidance for
# creative UI work (no broad tsc/lint/test before visual approval) and clean-diff
# expectations. false = keep the evidence nudge terse.
"verify_guidance": True,
# Max consecutive `pre_verify` "continue" nudges per turn (hooks can't trap the loop).
"max_verify_nudges": 3,
# Verification closure: after code edits in a workspace, refuse a final answer until fresh
# verification evidence exists or the agent explains why it can't check (bounded loop,
# passive ledger). False (default) because the nudges proved more noise than signal; true =
# force on everywhere; "auto" = on for interactive coding surfaces and programmatic callers,
# off for messaging surfaces. Doc/markdown/skill-only edits never fire.
"verify_on_stop": False,
# Inactivity warning (seconds), once per run before gateway_timeout; no interrupt. 0 = off.
"gateway_timeout_warning": 900,
# Max seconds any surface (CLI, TUI/Desktop, messaging gateway) blocks an agent awaiting a
# clarify-tool reply; then it unblocks with "[user did not respond within Xm]". 0 or less =
# unlimited. Resolved by tools/clarify_gateway.py::resolve_clarify_timeout (a legacy
# top-level ``clarify.timeout`` still wins when explicitly set).
# 1h because users step away and a shorter value evicted the entry mid-think so a later
# button tap hit a dead entry. Tradeoff: a higher value holds the gateway's running-agent
# guard longer for a genuinely abandoned prompt — lower it to free the guard sooner. See #32762.
"clarify_timeout": 3600,
# "Still working" status interval (seconds); 0 = off. Lower = faster feedback, more noise;
# 180 catches spinning weak-model runs before users /restart.
"gateway_notify_interval": 180,
# Session stall watchdog (seconds): RECOVERY notifier for an in-process AIAgent with an
# adapter-queued follow-up while its activity clock is stale — NOT a general stall detector
# (ignores startup restore, build sentinels, leases, debounce, other processes; scan cadence
# per AIAgent). Notify-only: tells the user to try /new. Distinct from gateway_timeout
# (kills the turn) and gateway_notify_interval. 0 = disable.
# See #76354.
"session_stall_timeout": 300,
# Transcript-sanitiser heal escalation: after this many pre-send heal passes within a
# 10-minute window, log one ERROR and queue a ONE-TIME out-of-band notice pointing at /debug
# share or `hermes doctor` (status channel only; prompt cache untouched). 0 = no escalation
# (per-window WARNINGs still fire).
# See #96870.
"sanitizer_heal_escalation_threshold": 3,
# Seconds of continuous reconnect failure before a platform gets needs_attention flagged in
# gateway status (`hermes status` / fleet monitoring). Retries never stop — a signal, not a
# circuit breaker. 0 = disable.
"reconnect_attention_after": 7200,
# Freshness window (seconds) for the auto-continue note. After a crash/restart mid-run the
# next user message gets "[System note: your previous turn was interrupted...]" prepended;
# only when the last persisted transcript row is younger than this, so stale markers don't
# revive an unrelated old task. Covers gateway_timeout (1800) plus slack. 0 = always inject.
"gateway_auto_continue_freshness": 3600,
# Max seconds the gateway waits for boot auto-resume turns before releasing the
# startup-restore inbound gate (all inbound is QUEUED while shut, so one long resumed turn
# would leave every channel unanswered). On timeout the gate opens and the resume keeps
# running in the background; duplicate-agent protection is unaffected because the resume
# slot is claimed synchronously first. 0 = wait forever.
"gateway_startup_restore_drain_timeout": 30,
# Max seconds the boot turn-machinery warm-up (run_agent import graph, tool schemas +
# availability probes, context-file tier) may hold the inbound gate shut, so an early
# message isn't served with a skeleton system prompt. On timeout the gate opens and warm-up
# finishes in the background. 0 = disable warm-up (lazy init).
"gateway_startup_warmup_timeout": 20,
# Stale-stream ceiling (seconds) for local providers (Ollama, oMLX, llama-cpp). Applied when
# the base stale timeout is at its 180s default and a local endpoint is detected, so a
# wedged local server eventually trips the detector instead of hanging forever. Env
# HERMES_LOCAL_STREAM_STALE_TIMEOUT overrides.
"local_stream_stale_timeout": 900,
# How user-attached images reach the main model (gateway, TUI, CLI /attach). "auto" = native
# when the model reports supports_vision=True AND auxiliary.vision.provider is not
# explicitly set, else text; "native" = always attach (non-vision models error at the
# provider or get a last-chance text fallback); "text" = always pre-analyze with
# vision_analyze and prepend the description. vision_analyze stays a tool regardless.
"image_input_mode": "auto",
"disabled_toolsets": [],
# Model name (any reasonable spelling) -> effort level; overrides agent.reasoning_effort
# when the current model matches. Edit in config.yaml (no CLI support: dots in keys).
"reasoning_overrides": {},
# Preserve assistant `reasoning_content` on history replay. Echo families (DeepSeek,
# Kimi/Moonshot, Xiaomi MiMo) are auto-detected by provider name/base-URL host; custom
# providers and OpenAI-compatible gateways proxying them are not. Set `reasoning_echo: true`
# on a `model:` entry or a `fallback_providers:` entry to opt in per provider. Default
# false: strict providers (Mistral, Groq, Cerebras) reject the field.
"reasoning_echo": False,
# Turn liveness watchdog: a turn with no observable progress for `timeout_s` seconds is
# logged, force-interrupted so the UI can retry, and its lease stops renewing so stale-turn
# cleanup can reclaim the session even if the interrupt can't unwind a wedged frame.
# timeout_s <= 0 disables; poll_s = sampling interval. Invalid values (NaN, Inf,
# non-positive poll) warn and fall back to defaults. See agent/turn_liveness.py.
"turn_liveness": {"timeout_s": 600.0, "poll_s": 15.0},
},
"terminal": {
"backend": "local",
"modal_mode": "auto",
# Remote-backend connection-class failures (SSH host unreachable, Docker daemon down):
# "warn" = structured degraded tool result with reason + retry hint; "fail" = raise error +
# traceback.
"degraded_mode": "warn",
"cwd": ".", # Use current directory
# Root for terminal session temp files (background logs/pid/exit files, code-exec
# sandboxes). Empty = TMPDIR/TMP/TEMP if set, else HERMES_HOME/cache/terminal (auto-pruned
# after 72h) — NOT tmpfs /tmp, which is RAM-capped and fills under load. Must be an existing
# absolute POSIX path; user-set paths are never auto-pruned.
"temp_dir": "",
# CSS font-family for the desktop app's xterm.js terminal (e.g. "'CaskaydiaCoveNerdFont',
# monospace"). Empty = built-in default ("'JetBrains Mono', 'Cascadia Code', 'SF Mono',
# Menlo, Consolas, monospace"). Lets users use a Nerd Font without patching the app.
"font_family": "",
"timeout": 180,
# Seconds between SIGTERM and escalated SIGKILL for host process trees (browser daemons). 0
# = SIGTERM only.
"daemon_term_grace_seconds": 2.0,
# Max seconds a one-shot CLI run (-q/-Q/-z) lingers for tracked notify_on_complete
# background processes to finish. The dying parent owns their stdout pipes, so exiting
# immediately kills the delivery (e.g. Bot Mode handoff replies via message_agent /
# bot_relay). Plain background processes without notify_on_complete are never waited on. 0
# disables.
# Bounded linger (seconds) for one-shot CLI runs (-q/-Q/-z) that exit while background processes
# spawned with notify_on_complete=true are still running. See #90879.
"oneshot_completion_wait_seconds": 600.0,
# Env vars passed into sandboxed terminal/execute_code (skill-declared
# required_environment_variables pass through automatically).
"env_passthrough": [],
# Remote-backend sync-back refuses to extract a downloaded state archive larger than this
# (bytes); raise it for a ~/.hermes tree that legitimately exceeds 2 GiB.
"sync_back_max_bytes": 2 * 1024 * 1024 * 1024,
# HOME for host tool subprocesses: "auto" = host keeps the real OS-user HOME, containers use
# HERMES_HOME/home; "real" = force real HOME; "profile" = force HERMES_HOME/home when it
# exists (strict per-profile isolation).
"home_mode": "auto",
# Extra files sourced in the login shell when building the per-session env snapshot — for
# nvm/pyenv/asdf/PATH entries registered by files a bash login shell skips (~/.bashrc,
# ~/.zshrc, ~/.zprofile). Supports ~ and ${VAR}; missing files skipped. When empty and the
# shell is bash, ~/.profile, ~/.bash_profile, ~/.bashrc are auto-sourced in that order (see
# auto_source_bashrc).
"shell_init_files": [],
# Source ~/.profile, ~/.bash_profile, ~/.bashrc in the snapshot login shell to capture PATH
# additions, functions, and aliases that `bash -l -c` misses (bash skips bashrc when
# non-interactive; Debian/Ubuntu ~/.bashrc short-circuits). ~/.profile and ~/.bash_profile
# go first because n/nvm/asdf write PATH exports there without an interactivity guard. Turn
# off if an rc file misbehaves when sourced non-interactively (exits on TTY check).
"auto_source_bashrc": True,
"docker_image": "nikolaik/python-nodejs:python3.14-nodejs22",
"docker_forward_env": [],
# Exact key-value env pairs set inside Docker containers (unlike docker_forward_env, which
# reads host values) — useful under systemd without the user's shell env. Example:
# {"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.sock"}
"docker_env": {},
"singularity_image": "docker://nikolaik/python-nodejs:python3.14-nodejs22",
"modal_image": "nikolaik/python-nodejs:python3.14-nodejs22",
"daytona_image": "nikolaik/python-nodejs:python3.14-nodejs22",
"vercel_runtime": "node24", # vercel_sandbox backend only: node24 | node22 | python3.13
# Container limits (docker, singularity, modal, daytona, vercel_sandbox; not local/ssh).
"container_cpu": 1,
"container_memory": 5120, # MB (default 5GB)
"container_disk": 51200, # MB (default 50GB)
"container_persistent": True, # Persist filesystem across sessions
# Docker volume mounts, "host_path:container_path" (docker -v syntax), e.g.
# ["/home/user/.hermes/cache/documents:/output"]. For gateway MEDIA delivery, write to
# /output/... inside Docker and emit the host-visible path in MEDIA:, not the container one.
"docker_volumes": [],
"docker_mount_cwd_to_workspace": False, # mount host cwd at /workspace (weakens isolation)
"docker_network": True, # false = --network=none, no network access from commands
"docker_extra_args": [], # Extra flags passed verbatim to docker run
# /dev/shm size for the Docker sandbox. Docker's 64 MB default silently breaks
# Chromium/Playwright and PyTorch DataLoader workers; tmpfs is lazily allocated so the
# higher ceiling is free until used. "" or "0" = omit the flag (Docker default).
"docker_shm_size": "1g",
# Run the container as the host uid:gid (`--user`) so files written to bind mounts
# (docker_volumes, persistent workspace, mounted cwd) are owned by you, not root. Off by
# default for images whose entrypoints must start as root (e.g. the bundled Hermes image,
# which drops to `hermes` via s6-setuidgid). When on, SETUID/SETGID caps are omitted.
"docker_run_as_host_user": False,
# Snap-packaged Docker under AppArmor (Ubuntu cloud images; LP#1908448) refuses to exec
# anything under `--init` or `--security-opt no-new-privileges` ("operation not
# permitted"). True drops those two flags; every other hardening stays. See #9730.
"docker_snap_compat": False,
# Trusted profiles sharing one Docker container identity; empty = per-profile boundary.
"docker_shared_container_key": "",
# Keep a long-lived bash shell across execute() calls so cwd/env/shell variables survive.
# Applies to non-local backends (SSH); local is opt-in via TERMINAL_LOCAL_PERSISTENT env.
"persistent_shell": True,
},
"web": {
"backend": "", # shared fallback — applies to both search and extract
"search_backend": "", # per-capability override for web_search (e.g. "searxng")
"extract_backend": "", # per-capability override for web_extract (e.g. "native")
# per-page char budget for web_extract; larger pages truncate, full text kept in cache/web
"extract_char_limit": 15000,
# Keyless free-tier ring: with NO web backend configured or keyed, web_search/web_extract
# rotate round-robin across exa, parallel, firecrawl, keenable public free tiers, failing
# over on rate limits. Never pre-empts a configured/keyed backend. false = disable.
"keyless_fallback": True,
# One-shot rescue: when the chosen/keyed backend fails a call, THAT call retries once on the
# keyless ring; the next call tries the chosen backend again (no sticky failover). Off when
# keyless_fallback is false.
"keyless_rescue": True,
# Per-vendor tier for vendors with both a keyless free endpoint and a keyed paid path (exa,
# parallel, firecrawl, keenable; tavily is opt-in keyless via `hermes tools`, not a ring
# member). Set by the `hermes tools` picker. "free" = always anonymous endpoint even with a
# key; "paid" = always keyed (missing key = error; vendor excluded from the ring); unset =
# keyed when the key is present, else the ring.
"provider_tier": {},
# TTL caching for web_search + web_extract: repeat searches (same query + provider) within
# the TTL come from an in-process memo; repeat extracts from the cache/web store. Concurrent
# identical searches coalesce into one vendor request. Only successes cached.
"cache_enabled": True,
"cache_ttl_minutes": 20,
# Hosts always fetched live, never from the extract cache (staging deploys, tunnel URLs,
# preview builds). Entries match exactly, as "*.wildcard", or as a domain suffix
# ("mysite.dev" also covers "preview.mysite.dev"). localhost/private IPs always exempt.
"cache_exempt_hosts": [],
},
"browser": {
# "" = Browser Use mode when the browser-use CLI (or uvx) is available, else built-in tools
# (Camofox setups always keep built-in tools: no CDP surface); "browser-use" = force one
# browser_exec tool driving the Browser Use CLI over any CDP backend (local Chrome, cloud);
# "off" = force the built-in browser_navigate/browser_click/... tools.
"backend": "",
"inactivity_timeout": 120,
"command_timeout": 30, # seconds per browser command (screenshot, navigate, etc.)
"snapshot_threshold": 15000, # max chars before snapshot truncate-and-store (min 1000)
"record_sessions": False, # auto-record browser sessions as WebM videos
# headed: visible Chromium window (local); skips per-turn cleanup, idle reaper still applies
"headed": False,
"allow_private_urls": False, # allow private/internal IPs (localhost, 192.168.x.x, ...)
# Local browser engine for both drivers. "auto" = Chrome; "lightpanda" = faster navigation,
# no screenshots (Browser Use mode spawns `lightpanda serve` per session; built-in tools
# pass `--engine <value>` to agent-browser with Chrome fallback); "chrome" = explicit.
# Ignored while a cloud provider, Camofox, cdp_url or use_real_profile is active. Also
# settable via AGENT_BROWSER_ENGINE.
"engine": "auto",
# With a cloud provider, auto-spawn local Chromium for LAN/localhost URLs instead
"auto_local_for_private_urls": True,
"cdp_url": "", # persistent CDP endpoint for attaching to an existing Chromium/Chrome
# Consent to browse with the user's REAL logins locally: runs on a Hermes-managed SNAPSHOT
# of the ACTIVE default-Chromium profile (Local State -> profile.last_used; cookies, logins,
# prefs copied and re-synced per fresh session) driven by Hermes' packaged Chromium. The
# snapshot dir sidesteps Chrome 136+'s default-profile debugging block and never contends
# with the running browser. Turning off deletes ~/.hermes/browser-profile/ so credentials
# don't outlive consent. Chromium-family only (Chrome, Edge, Brave, Brave Origin, Chromium);
# Firefox etc. fails closed. Also gates the browser_exec `local` argument (real-profile
# local session even under a cloud backend). Desktop Settings -> Browser.
"use_real_profile": False,
# Windows only: a running Chrome/Edge/Brave locks its cookie DB, so the profile can't be
# copied. When on, a locked profile still blocks and the agent ASKS first; on approval it
# runs `hermes browser close-profile` (kills that profile's browser tree, unsaved tabs lost)
# and retries once; still locked -> stays blocked, no auto-kill. No effect on macOS/Linux,
# where a running browser instead makes the Login Data / Web Data SQLite backups miss
# their deadline; quit the browser by hand there.
"real_profile_autoclose": False,
# Pin WHICH source profile directory is snapshotted for real-profile browsing (e.g. "Profile
# 2"). Empty = browser's last-used profile, which on multi-profile machines can hand the
# agent the wrong identity. A pin naming a missing directory FAILS CLOSED.
"real_profile_pin": "",
# restrict_evaluate: opt-in denylist blocking sensitive JS primitives (cookies/storage/
# clipboard/network/form values) in browser_console(expression=...); allow_unsafe_evaluate
# is the legacy override that bypasses that denylist entirely.
"allow_unsafe_evaluate": False,
"restrict_evaluate": False,
# CDP supervisor: dialog + frame detection over a persistent WebSocket; active only with a
# CDP-capable backend (Browserbase, or local Chrome via /browser connect). See
# website/docs/developer-guide/browser-supervisor.md.
"dialog_policy": "must_respond", # must_respond | auto_dismiss | auto_accept
"dialog_timeout_s": 300, # safety auto-dismiss after N seconds under must_respond
"camofox": {
# true = send a stable profile-scoped userId so Camofox maps it to a persistent Firefox
# profile; false = random ephemeral userId per session.
"managed_persistence": False,
# Externally managed Camofox identity, for when another app owns the visible browser.
"user_id": "",
"session_key": "",
"adopt_existing_tab": False, # rehydrate tab_id from Camofox before creating a tab
# Docker Camofox opens page URLs from inside the container: rewrite loopback page URLs
# (localhost/127.0.0.1/::1) to the host alias; CAMOFOX_URL itself is unchanged.
"rewrite_loopback_urls": False,
"loopback_host_alias": "host.docker.internal",
},
# Authenticated browser-extension controller lane: a registered extension can become the
# exact controller for a session's browser_* tools (fail-closed once bound). Local API
# registration also requires the API server bearer key. developer_mode gates the privileged
# browser_cdp / browser_evaluate capabilities.
"extension_control": {"enabled": False, "developer_mode": False},
},
# Filesystem checkpoints: snapshot the working directory once per turn (on the first
# write_file/patch call); restore with /rollback. Opt-in via `hermes chat --checkpoints` or
# enabled=True (most users never use /rollback). Single shared shadow store with real pruning.
"checkpoints": {
"enabled": False,
# Max checkpoints per working directory; enforced by ref rewrite + GC of older commits.
"max_snapshots": 20,
# Hard ceiling on total ~/.hermes/checkpoints/ size (MB); the oldest checkpoint per project
# is dropped round-robin until under the cap. 0 disables.
"max_total_size_mb": 500,
# Skip files larger than this (MB) when staging (datasets, model weights). 0 = no filter.
"max_file_size_mb": 10,
# Background sweep (CLI helper thread / gateway housekeeping tick, at most once per
# min_interval_hours; never on the startup path — its git gc can block for tens of seconds):
# deletes projects whose last_touch is older than retention_days, GCs the shared store when
# refs moved, enforces max_total_size_mb, deletes legacy-* archives older than retention_days.
# It NEVER deletes orphans (workdir missing on
# disk) — a missing workdir may just be an unmounted volume/VPN, and an unattended sweep
# must not guess. Orphans: `hermes checkpoints prune` (`--keep-orphans` to skip).
"auto_prune": True,
"retention_days": 7,
"min_interval_hours": 24,
},
# Hard cap (chars) for one auto-loaded context file (SOUL.md, AGENTS.md, CLAUDE.md, .hermes.md,
# .cursorrules) before head/tail truncation. null = scale with the model's context window (floor
# 20K, ceiling 500K); a positive int pins a fixed cap. Separate from read_file limits.
"context_file_max_chars": None,
# Seconds to wait for a single context file read before skipping it with a warning. Guards startup
# against network-backed filesystems (iCloud Drive, OneDrive, NFS) that can block a cold read.
"context_file_read_timeout": 5.0,
# Max chars per read_file call; larger reads are rejected with offset+limit guidance. 100K chars
# ≈ 25–35K tokens.
"file_read_max_chars": 100_000,
# Seconds the first agent build waits for background MCP discovery before snapshotting its tool
# list. Returns the instant discovery completes (no MCP servers → ~0s); the bound only bites
# when a server is still connecting. Turn-1 latency knob only: a server that misses it is picked
# up by the between-turns refresh (agent/turn_context.py), so keep it small — a dead server adds
# this much to first-response latency.
"mcp_discovery_timeout": 1.5,
# Same bound for single-query mode (``hermes -q/-z``). With only ONE turn there is no
# between-turns refresh, so a server that misses the window is invisible for the whole session;
# the larger bound lets slow cold-start servers (npx, uvx, remote HTTP) land. Reachable servers
# still only wait their real handshake time.
"mcp_single_query_discovery_timeout": 15.0,
"mcp": { # MCP runtime behavior (distinct from mcp_servers: definitions and auxiliary.mcp).
# Auto-reload MCP connections when config.yaml's mcp_servers changes (CLI watcher). Every
# reload rebuilds the tool surface and INVALIDATES the provider prompt cache (next message
# re-sends the full prefix) — costly on long-context models. When false the watcher still
# detects the change and prints /reload-mcp guidance.
"auto_reload_on_config_change": True,
# Max MCP servers connected (and their stdio child trees spawned) at once per discovery
# pass — at boot, on /reload-mcp and on the config watcher's reconcile. Unbounded, a config
# with N servers spawns N process trees in the same instant (RAM/CPU spike, 429 fan-out on
# multi-profile fleets). 0 = unlimited.
"discovery_concurrency": 4,
},
# Tool-output truncation. max_bytes: terminal_tool output cap in chars (head+tail kept; 50_000 ≈
# 12-15K tokens). max_lines: max `limit` one read_file call may request before clamping.
# max_line_length: per-line cap in read_file's line-numbered view (chars).
"tool_output": {"max_bytes": 50000, "max_lines": 2000, "max_line_length": 2000},
# Tool loop guardrails nudge models that repeat failed/non-progressing tool calls. Soft warnings
# are always on; hard stops are opt-in so interactive sessions keep flowing.
"tool_loop_guardrails": {
"warnings_enabled": True,
"hard_stop_enabled": False,
# Unattended gateway/cron platforms hard-stop by default (nobody can /stop a model that
# ignores warnings); interactive cli/tui/desktop/acp stay warning-only.
"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
},
# Per-turn hard ceilings for runaway-prone tools; counters reset every turn, always on
# regardless of the thresholds above. Dozens of searches/subagents in ONE turn is already
# pathological, hence low defaults. 0 = unlimited.
"loop_caps": {
"max_web_searches": 50, # web_search calls per turn
"max_subagents": 50, # subagents spawned per turn
},
},
"compression": {
"enabled": True,
# checkpoint_required: fail closed before lossy compaction unless an active memory provider
# confirms checkpoint API compatibility and completes the checkpoint.
"checkpoint_required": False,
# progress_notices: when True, routine compression progress statuses (compacting/
# preflight/pre-API/idle/retry) reach chat gateways instead of being filtered as noise.
# Failure notices and manual /compress feedback are always visible.
"progress_notices": False,
# threshold: compress when context usage exceeds this ratio. Models with windows below 512K
# are floored at 0.75 (raise-only) so compaction doesn't fire with half the window free; set
# above 0.75 to override the floor.
"threshold": 0.50,
# threshold_tokens: absolute token cap — compression triggers at the lower of the ratio
# threshold and this count. Clamped to the model's context length. 256K bounds 1M-window
# models (their 50% trigger sat at 500K, so compaction never fired) while every lower
# ratio trigger still wins; null = ratio-only.
"threshold_tokens": 256_000,
# "progress_notices": False, # opt-in (#52995): when True, routine compression
"target_ratio": 0.20, # fraction of threshold to preserve as recent tail
# tail_mode: "lean" = clamped 2.5%-of-window tail (10K floor / 25K cap) plus chunked
# digests, anchor index, verbatim user messages and session_search pointers in the summary
# (~3x fewer retained tokens; a few extra summarizer calls at the boundary). "legacy" =
# 0.20×threshold verbatim tail (100-240K tokens on big windows).
"tail_mode": "lean",
# protect_last_n: minimum recent messages kept uncompressed, honoured up to a small count
# floor; the verbatim tail is otherwise token-bounded and never above 20% of the window.
"protect_last_n": 20,
# min_tail_user_messages: REAL (actionable) user messages guaranteed to survive in the tail.
# 1 = single last-user anchor; raise (e.g. 3) when bulky tool outputs fill the tail budget.
"min_tail_user_messages": 1,
# max_attempts: retry rounds before a turn gives up with "max compression attempts reached".
# Raise (e.g. 6) for tool-schema-heavy sessions. Validated >= 1, cap 10.
"max_attempts": 3,
# proactive_prune_tokens: opt-in trigger (tokens) for the deterministic no-LLM tool-result
# prune, independent of `threshold` (which rarely fires on large windows, so old tool output
# is re-sent every turn); e.g. 48000 reclaims early. 0 = off. Tail protected by
# `protect_last_n`. Built-in compressor only. Each committed prune rewrites sent history and
# breaks the prompt-cache prefix — the min_reclaim gate below keeps those breaks episodic.
"proactive_prune_tokens": 0,
# Prune's summarize pass only touches tool results larger than this (chars); clamped >= 200
# so a generated summary can't be re-summarized.
"proactive_prune_min_result_chars": 8000,
# A prune only commits when it reclaims at least this many tokens, then waits for a
# trigger-sized runway to regrow before rearming. 0 = no minimum-savings gate.
"proactive_prune_min_reclaim_tokens": 4096,
# micro_compact: opt-in — after each turn fold the oldest un-absorbed exchange into a
# rolling summary, amortizing compression cost. Off by default because every pass rewrites
# sent history and breaks the prompt-cache prefix EVERY turn; enable only if the amortized
# stall beats the cached-prefix discount. See website/docs/developer-guide/micro-compaction.md.
"micro_compact": False,
# Cadence: run a pass every Nth completed turn (1 = one cache break per turn, 5 = a fifth of
# the breaks). Clamped >= 1; ignored unless micro_compact is true.
"micro_compact_every_n_turns": 1,
# Once the rolling summary exceeds this many tokens, the next pass re-summarizes it.
"micro_compact_defrag_threshold_tokens": 2000,
# Gateway session-hygiene force-compress threshold, by message count.
"hygiene_hard_message_limit": 5000,
# Max seconds the gateway waits for pre-agent hygiene compression WITHOUT forward progress.
# Inactivity budget: a slow model still streaming tokens extends the wait.
"hygiene_timeout_seconds": 30,
# Absolute cap on the hygiene wait even while tokens are moving (bounds a trickle stream).
# Clamped >= hygiene_timeout_seconds.
"hygiene_total_ceiling_seconds": 600,
"hygiene_failure_cooldown_seconds": 300, # skip repeated failed hygiene attempts
# Max seconds an ARRIVING user turn is held while a streaming hygiene summary finishes;
# bounds user-visible latency (keep under chat idle timeouts, Telegram ~30s). On expiry the
# turn proceeds uncompressed; the detached worker keeps its watermark-fenced commit, so the
# summary is adopted at the next safe boundary.
"hygiene_max_turn_hold_seconds": 10,
# Inactivity budget for in-agent compress_context (loop, /compress, preflight); same
# progress-aware semantics as hygiene_timeout_seconds. 0 = disable the owned wrapper
# (callers passing commit_fence, e.g. gateway hygiene, never use it). Floored at the auxiliary
# compression request timeout (auxiliary.compression.timeout, min 300s): the host never judges
# silence before the summary request itself would time out.
"context_timeout_seconds": 120,
# Absolute cap on the *pre-commit* compress_context wait (summary/stream phase) even while
# tokens move. Clamped >= context_timeout_seconds when that is > 0. A started SessionDB
# commit is never abandoned: past the ceiling it is logged (WARNING, then ERROR) and
# surfaced on the warning channel while the host keeps waiting.
"context_total_ceiling_seconds": 600,
# Non-system head messages always kept verbatim, in ADDITION to the (always protected)
# system prompt. 0 = pin nothing but system prompt + summary + tail.
"protect_first_n": 3,
# When True, auto-compression whose summary fails (aux error / non-JSON / timeout) aborts
# instead of dropping the middle with a "summary unavailable" placeholder; the session
# freezes at its size until /compress (bypasses the cooldown) or /new.
"abort_on_summary_failure": False,
# (Historical key name.) When True, gpt-5.4/5.5/5.6 and gpt-6 Astra (any slug containing
# "astra" without "900k") on the ChatGPT Codex OAuth route raise their compaction trigger to
# 85%: Codex hard-caps them at a 272K window, so the global 50% would compact at ~136K. False = global `threshold`. Only that route; the same models via
# OpenAI direct, OpenRouter or Copilot keep the global value.
"codex_gpt55_autoraise": True,
# Show the one-time autoraise banner; False keeps the autoraise, hides the notice.
"codex_gpt55_autoraise_notice": True,
# Codex app-server thread compaction mode. The codex agent owns the thread context, so
# Hermes' summarizer cannot shrink it. native = codex decides; hermes = Hermes' threshold
# triggers thread/compact/start; off = never auto-trigger.
"codex_app_server_auto": "native",
# Opt in to OpenAI server-side compaction on the Responses API. Only gpt-5.6-family on
# api.openai.com or the Codex backend; local compression stays as fallback.
"codex_responses_native": False,
# Absolute server compaction trigger (input tokens). None follows the local trigger with a
# safety margin; explicit values only clamp downward so the server goes first.
"codex_responses_compact_threshold": None,
# in_place: compaction rewrites the message list and system prompt WITHOUT rotating the
# session id (no parent_session_id chain, no `name #N` renumbering), avoiding the
# session-rotation bug cluster. Pre-compaction turns are soft-archived under the same id
# (active=0, compacted=1) — still session_search-able. False = legacy rotating-compaction
# path.
"in_place": True,
# Per-model threshold overrides: keys substring-match the model name (longest wins), values
# replace the global `threshold`, e.g. {"glm-5.2": 0.40}. Prefix a key with "<provider>:" to
# scope it to one route ({"openai-codex:astra": 0.85} leaves Astra on OpenRouter/Nous at the
# global value). The <512K floor (0.75) still applies raise-only on top.
"model_thresholds": {},
# Opt-in idle compaction (0 = off): a session resuming after this many idle seconds compacts
# up front, before the first reply. Time-based complement to `threshold`; skipped when
# already at/below threshold × target_ratio; honors the same cooldown/ anti-thrash/lock
# guards. Example: 1800 = 30 min.
"idle_compact_after_seconds": 0,
},
# Anthropic prompt caching (Claude via OpenRouter or native API). cache_ttl: "5m" | "1h" | "auto"
# (auto = 1h for human-paced sessions — cli/tui/desktop/messaging — and 5m for subagent, cron,
# oneshot, webhook, kanban, api, tool, batch); other non-falsy values are ignored; falsy (false, null, "off",
# "disabled", "no", "none") disables caching.
"prompt_caching": {"cache_ttl": "5m"},
# OpenRouter settings. response_cache: X-OpenRouter-Cache header — identical requests return
# cached responses at zero billing; independent of Anthropic prompt caching. response_cache_ttl:
# seconds (1-86400), only used when response_cache is on. min_coding_score (0.0-1.0):
# pareto-code router knob, applied only when model.model is "openrouter/pareto-code"; higher =
# stronger/pricier coders, 0.65 = mid-tier, "" = let OpenRouter pick the strongest. Docs:
# openrouter.ai/docs/guides/routing/routers/pareto-router
"openrouter": {"response_cache": True, "response_cache_ttl": 300, "min_coding_score": 0.65},
"bedrock": { # AWS Bedrock; only used when model.provider is "bedrock".
"region": "", # empty = AWS_REGION env var → us-east-1
"discovery": {
"enabled": True, # auto-discover models via ListFoundationModels
"provider_filter": [], # restrict to these providers, e.g. ["anthropic", "amazon"]
"refresh_interval": 3600, # cache discovery results (seconds)
},
# Bedrock Guardrails: create one in the console, then set ID and version.
# https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html
"guardrail": {
"guardrail_identifier": "", # e.g. "abc123def456"
"guardrail_version": "", # e.g. "1" or "DRAFT"
"stream_processing_mode": "async", # "sync" | "async"
"trace": "disabled", # "enabled" | "disabled" | "enabled_full"
},
},
# Auxiliary model config — provider/model per side task. provider "auto" = auto-detect;
# empty model = provider's default aux model; all tasks fall back to
# openrouter:google/gemini-3-flash-preview when the configured provider is unavailable.
# extra_body is forwarded verbatim as request body fields for that task, e.g. OpenRouter
# routing prefs / Pareto Code floor:
# auxiliary:
# compression:
# extra_body:
# provider: {order: [anthropic, google], sort: throughput} # or price | latency
# plugins: [{id: pareto-router, min_coding_score: 0.5}]
# Each task is independent — main-agent provider_routing and openrouter.min_coding_score
# do NOT propagate to aux calls by design.
"auxiliary": {
# Same-provider retries for a transient blip (reset/timeout/5xx/408) on ANY aux call before
# falling back; clamped [0,6]. Matters for pinned calls (MoA advisors) where provider
# fallback is not meaningful recovery.
"transient_retries": 2,
# When true, the auto-chain's OpenRouter step is skipped unless the fallback model ends in
# ":free" — a PAID lane is never used for background aux traffic even with
# OPENROUTER_API_KEY set.
"free_only": False,
# Override the auto-chain's OpenRouter fallback model (default google/gemini-3.6-flash,
# PAID). Pair e.g. "nvidia/nemotron-3-ultra-550b-a55b:free" with free_only: true. A one-time
# WARNING is logged whenever a non-":free" model is engaged.
"openrouter_model": "",
# Endpoints that reject NON-streaming chat (HTTP 400): aux calls are sent with stream=True
# and aggregated. Case-insensitive URL substrings; copilot.tencent.com is always
# stream-only.
"stream_only_base_urls": [],
# Per-task blocks share one shape (_aux): provider "auto" = inherit the main model; base_url
# overrides provider; api_key falls back to OPENAI_API_KEY; reasoning_effort:
# none|minimal|low|medium|high|xhigh|max|ultra ("" = provider default); extra_body =
# OpenAI-compatible request fields. Vision: download_timeout = image HTTP download (s).
"vision": _aux(120, download_timeout=30),
# web_extract and session_search no longer use an aux LLM; leftover blocks in user config
# are ignored. Compression: raise timeout for local models. no_progress_timeout
# (Codex/Responses streams only): seconds without a substantive event before the stream
# fails fast; None = built-in 60s default. Independent of "timeout" (the overall request
# budget) — raising "timeout" alone does not widen this window. See #108104.
"compression": _aux(120, no_progress_timeout=None),
"skills_hub": _aux(30),
"approval": _aux(30), # classifier — a fast/cheap model is recommended
# /review reviewer: a full subagent on the async delegation rail, credentials resolved like
# delegation.provider pins. "auto" + "" = main agent's model. api_mode forces transport:
# chat_completions | anthropic_messages | codex_responses.
"review": {"provider": "auto", "model": "", "base_url": "", "api_key": "", "api_mode": ""},
"mcp": _aux(30),
# prefer_fast_model opts in to the provider fast tier; auto otherwise = main model.
"title_generation": {
"enabled": True,
"model_upgrade_enabled": True, # False = keep the instant derived title, never call a model
# Note: session_search no longer uses an auxiliary LLM (PR #27590 — single-shape tool returns DB
# content directly). The old ``auxiliary.session_search.*`` block was removed here. Existing
# values in user config.yaml files are harmless leftovers and ignored.
"provider": "auto",
"model": "",
"prefer_fast_model": False,
"base_url": "",
"api_key": "",
"timeout": 30,
"extra_body": {},
"reasoning_effort": "",
"language": "",
},
"memory_query_rewrite": _aux(8, reasoning_effort=False),
"tts_audio_tags": _aux(30),
# Kanban: triage_specifier expands a Triage one-liner into a spec (cheap model OK);
# kanban_decomposer emits a JSON graph of child tasks (more tokens).
"triage_specifier": _aux(120),
"kanban_decomposer": _aux(180),
"profile_describer": _aux(60), # 1-2 sentence profile blurb; short, cheap
"goal_judge": _aux(60), # /goal satisfaction + contract drafting; JSON calls
# Curator skill-usage review can take minutes on reasoning models (umbrellas over hundreds
# of skills); route cheaper via `hermes model` → auxiliary → Curator.
"curator": _aux(600),
"monitor": _aux(60), # important-mail 0-10 scorer; high-volume, small model fine
# Post-turn self-improvement fork (save memory / patch skill). "auto" = main model replaying
# the full conversation (warm cache); other models replay a compact digest (~3-5x cheaper).
# enabled=false skips auto spawns (/refine still works). An explicit max_input_tokens caps
# the SUM of replayed input tokens over the review loop (iterations capped at 16); the loop
# stops before crossing it. When unset, the runtime derives a budget from the active model
# context window. <= 0 = unlimited.
# reasoning_effort is IGNORED while the review stays on the main model: the fork inherits the
# conversation's reasoning config verbatim so its request bytes keep the parent's warm
# prompt-cache prefix (#30532). Set provider/model below to route the review to another model
# if you want a different effort level; a one-time warning says so when the key is set.
"background_review": {"enabled": True, **_aux(120)},
# No reasoning_effort on MoA blocks by design — configured PER SLOT in the preset
# (moa.presets.<name>.reference_models[].reasoning_effort / aggregator.reasoning_effort).
"moa_reference": _aux(900, reasoning_effort=False),
"moa_aggregator": _aux(900, reasoning_effort=False),
},
"display": {
"compact": False,
"personality": "",
"resume_display": "full",
# Recap tuning for /resume and startup resume.
"resume_exchanges": 10, # max user+assistant pairs to show
"resume_max_user_chars": 300, # truncate user message text
"resume_max_assistant_chars": 200, # truncate non-last assistant text
"resume_max_assistant_lines": 3, # truncate non-last assistant lines
# Skip tool-call-only assistant entries in the recap so it isn't dominated by `[2 tool
# calls: ...]` lines; False shows them inline.
"resume_skip_tool_only": True,
"busy_input_mode": "interrupt", # interrupt | queue | steer
# steer mode: false hides only the "Steered into current run" bubble; steering itself still
# happens.
"busy_steer_ack_enabled": True,
# Classic CLI multiline beyond Alt+Enter: Ctrl+J newline, trailing backslash+Enter
# continues, Shift+Enter reported distinctly. False restores the c-j submit fallback for
# POSIX PTYs whose plain Enter arrives as LF.
"cli_multiline_shortcuts": True,
# Interface bare `hermes`/`hermes chat` launches: "cli" (prompt_toolkit REPL) | "tui" (Ink).
# Flags win: `--cli` forces the REPL, `--tui` / HERMES_TUI=1 forces the TUI.
"interface": "cli",
# Native TUI uses the terminal's primary buffer and scrollback instead of the custom
# alternate-screen viewport. Flags win: `--native` / `--tui-native` and `--cli`.
"tui_native": False,
# `hermes --tui` auto-resumes the most recent human-facing session (like `hermes -c`).
# HERMES_TUI_RESUME=<id> always wins.
"tui_auto_resume_recent": False,
# Desktop reopens the last chat/page on cold start (also in Settings → Appearance).
"resume_last_session": True,
# One-time TUI hint ("subagents working · /agents to watch live") on first delegation.
"tui_agents_nudge": True,
"bell_on_complete": False,
"bell_on_prompt": False, # bell when a blocking prompt opens (clarify/approval/sudo)
# Stream reasoning live before the response; otherwise thinking models show only a spinner
# for tens of seconds.
"show_reasoning": True,
# Post-response "Reasoning" recap collapses to 10 lines; true prints it all (live streaming
# is always full).
"reasoning_full": False,
# Background self-improvement notices in chat: "off" (review still runs) | "on" (generic "💾
# Memory updated") | "verbose" (content preview). Per-platform via
# display.platforms.<platform>.memory_notifications.
"memory_notifications": "on",
# Gateway notices when a terminal(background=true) process finishes: "concise" (one line;
# failures append an output tail) | "all" (running updates + final raw output) | "result"
# (final raw only) | "error" (raw only on non-zero exit) | "off".
"background_process_notifications": "concise",
"streaming": False,
"timestamps": False, # message timestamps (CLI labels, TUI rows, desktop transcript)
"timestamp_format": "%H:%M", # strftime format, e.g. "%b-%d %H:%M"
"final_response_markdown": "strip", # render | strip | raw
# Preserve recent classic-CLI output across Ctrl+L, /redraw and resize clears; disable if an
# emulator misbehaves with replayed scrollback.
"persistent_output": True,
"persistent_output_max_lines": 200,
# Also clear terminal scrollback on classic-CLI full redraw/resize recovery; enable when a
# terminal/tmux stack stamps stale prompt chrome into scrollback.
"cli_rebuild_scrollback_on_redraw": False,
# Print a one-line summary of resolved modal prompts (approval/clarify) to scrollback.
"persist_prompts": True,
"inline_diffs": True, # inline diff previews for write_file/patch/skill_manage
# Append a one-line advisory to the final response when a write_file/patch failed this turn
# and was never superseded by a successful write to the same path (catches "half the
# parallel patches failed, model claims success").
"file_mutation_verifier": True,
# Nous credits status-bar notices (usage bands, grant-spent, depleted/restored). False mutes
# them; balance data and /usage keep working.
"credits_notices": True,
# Append a one-line explanation when a turn ends with no usable reply (empty after retries,
# truncated stream, pending tool result, iteration/budget limit) instead of the bare
# "(empty)" sentinel.
"turn_completion_explainer": True,
"show_cost": False, # $ cost in the status bar
"battery": False, # battery read-out first in status bar; no-op w/o battery
# Focus view (/focus): display-only. Pins tool_progress to "off", reports per-turn
# hidden-line count, pins a "focus" status segment. focus_saved_tool_progress holds the mode
# /focus off restores. Never affects what the model sees (focus_view.py).
"focus_view": False,
"focus_saved_tool_progress": "all",
"skin": "default",
# UI language for static messages (approval prompts, some gateway slash replies); not agent
# responses/logs/tool outputs. en, zh, ja, de, es, fr, tr, uk; unknown → en.
"language": "en",
# TUI busy indicator: kaomoji | emoji | unicode (braille) | ascii. `/indicator <style>`.
"tui_status_indicator": "kaomoji",
# Seconds between idle prompt_toolkit redraws in the classic CLI; keeps wall-clock
# status-bar read-outs ticking and the bottom chrome from going stale. 0 disables it if it
# fights terminal auto-scroll in non-fullscreen mode.
# See #45592.
"cli_refresh_interval": 1.0,
# Vi/vim keybindings in the CLI input composer (config-only, no slash command).
# Off by default, preserving prompt_toolkit's standard emacs bindings.
"vim_mode": False,
"user_message_preview": { # CLI: submitted user-message lines echoed to scrollback
"first_lines": 2,
"last_lines": 2,
},
# Gateway: natural mid-turn assistant status messages. Desktop: keep mid-turn narration
# between tool calls instead of collapsing to the final message.
"interim_assistant_messages": True,
# Engine warning/failure notifications stay visible unless an operator opts in.
# Does not suppress task results, manual commands, or existing logs.
"suppress_warning_notifications": False,
# Codex Responses commentary channel: true delivers completed commentary as mid-turn interim
# updates; false routes it to reasoning (visible only with show_reasoning).
"show_commentary": True,
"tool_progress_command": False, # enable /verbose command in messaging gateway
# display.tool_progress_overrides is deprecated (use display.platforms); a user-set value is
# still honored at runtime and folded into platforms by migration.
"tool_preview_length": 0, # max chars for tool call previews (0 = no limit)
# Human-phrased status labels for built-in tools ("Reading <file>") in CLI spinner and
# gateway/desktop tool-progress; custom/plugin/MCP tools use the raw preview.
"friendly_tool_labels": True,
# CLI-only post-turn line: "⋯ 12.4s · edited 2 files +18 -3 · read 4 files · ran 3
# commands". Never in quiet/non-interactive or gateway surfaces (own footer).
"turn_summary": True,
# CLI-only: cumulative turn output tokens on the live spinner ("· ↓ 1.2k tok").
"spinner_token_flow": True,
# Gateway tool-progress grouping where edits are supported: "accumulate" edits one bubble |
# "separate" one message per tool (noisier). Needs tool_progress enabled. Per-platform:
# display.platforms.<platform>.tool_progress_grouping.
"tool_progress_grouping": "accumulate",
# Custom long-running status phrases. Defaults: gateway/assets/status_phrases.yaml.
# `path`/`paths` = HERMES_HOME-relative YAML files/dirs (or conventional status_phrases.yaml
# / status_phrases/*.yaml). Keys: status, generic. mode: "append" (default) | "replace".
# Per-platform: display.platforms.<platform>.status_phrases.
"status_phrases": {},
# Reasoning summary rendering: "code" (💭 fenced block) | "blockquote" ("> ") | "subtext"
# ("-# " Discord small grey text; Discord's default). Per-platform via
# display.platforms.<platform>.reasoning_style.
"reasoning_style": "code",
# Auto-delete EphemeralReply system notices ("✨ New session started!", …) after N seconds
# where deletion is supported (Telegram; others ignore). Agent responses are never touched.
# 0 = disabled.
"ephemeral_system_ttl": 0,
# Per-platform display/streaming overrides; unset keys fall through to the global. Telegram
# has smooth native draft streaming (on); Discord/Slack only edit-based streaming, which
# flickers (off). Gap-fillers only: explicit user values win, and the global
# streaming.enabled master switch still gates everything.
"platforms": {
"telegram": {"streaming": True},
"discord": {"streaming": False},
"slack": {"streaming": False},
# WeCom native streaming (msgtype "stream" via aibot_respond_msg).
"wecom": {"streaming": True},
},
# Gateway runtime footer on the FINAL message, e.g. `model · 68% · ~/projects/hermes`.
# Per-platform: display.platforms.<platform>.runtime_footer.
"runtime_footer": {
"enabled": False,
# order shown; drop any to hide. Opt-in extras: latency, served_model (alias → the
# deployment a routing proxy reported / Hermes' fallback route).
"fields": ["model", "context_pct", "cwd"],
},
# CLI/TUI status bar fields. Non-empty = only listed fields show (built-in order kept,
# config controls visibility not ordering); empty = default set. Available: model,
# context_detail, context_pct, cache_hit, latency, tps, compressions, bg_tasks,
# bg_processes, bg_subagents, goal, git_branch (⎇ current branch, opt-in only), duration,
# prompt_elapsed, idle_since, focus, yolo,
# stash, battery, title, total_tokens (session Σ, opt-in only). Narrow terminals still drop
# context_detail/prompt_elapsed/idle_since.
"status_bar": {
"fields": [],
},
"copy_shortcut": "auto", # "auto" (platform default) | ctrl_c | ctrl_shift_c | disabled
# Petdex animated mascot (github.com/crafter-station/petdex): cosmetic sprite across
# CLI/TUI/desktop, managed with `hermes pets`. No effect on prompt caching.
"pet": {
"enabled": False,
"slug": "", # active pet slug in get_hermes_home()/pets/; empty → first installed
# auto (detect kitty/iTerm2/sixel, else unicode half-blocks) | kitty | iterm | sixel |
# unicode | off
"render_mode": "auto",
# Size scalar relative to native 192×208 frames, shared by desktop canvas and CLI/TUI
# column width. Half-block fallback clamps to a legibility floor.
"scale": 0.33,
"unicode_cols": 0, # Hard override for terminal column width; 0 = derive from scale.
},
},
"dashboard": {
# Visual theme: "default" | "midnight" | "ember" | "mono" | "cyberpunk" | "rose"
"theme": "default",
# Process-isolation rollout controls. Read via the raw config loader, so tui_gateway.server
# also owns explicit defaults.
"turn_isolation": False,
"compute_host_heartbeat_secs": 15,
"compute_host_respawn_max": 3,
# Token/cost analytics surfaces are hidden by default: the numbers are a local LOWER-BOUND
# estimate, not billing — only successful main-agent responses with a response.usage count;
# auxiliary calls, retries, fallbacks and cache writes are missed, so the total can be
# 10x-100x under the provider bill.
"show_token_analytics": False,
# IPs / bounded CIDRs of reverse proxies trusted to supply X-Forwarded-Proto/-For. Loopback
# always trusted; wildcards and /0 rejected (spoofing guard).
"trusted_proxies": [],
# WebSocket keepalive (seconds), NON-loopback binds only: loopback always disables the
# protocol ping so an event-loop stall never kills a healthy local connection.
"ws_ping_interval": 20.0,
"ws_ping_timeout": 20.0,
# Grace (seconds) before a WS-orphaned gateway session is interrupted/reaped after its
# client disconnects. 0 = park forever. Env: HERMES_TUI_WS_ORPHAN_REAP_GRACE_S.
"ws_orphan_reap_grace_s": 20.0,
# A detached RUNNING turn is only interrupted once its activity clock (API waits, stream
# tokens, tool heartbeats) has been idle this many seconds; an active turn runs to
# completion. Default = agent.turn_liveness.timeout_s. 0 = interrupt at grace.
# See #100325, #98028.
"ws_orphan_activity_stale_s": 600.0,
# On gateway boot, close tui/desktop/subagent rows orphaned by a dead gateway (start AND
# newest message older than HERMES_TUI_SESSION_TTL_S, default 6h) with
# end_reason='startup_orphan_reap'; otherwise they stay phantom "active" forever.
# Messaging-gateway and live sessions are never touched; swept rows stay resumable.
# The ws-orphan grace timer above is in-process, so a gateway restart (update, crash, systemd)
# leaves disconnected sessions ``ended_at IS NULL`` forever — phantom "active" rows in /resume and
# dashboards. See #65194.
"startup_orphan_sweep": True,
# OAuth gate (engaged when --host is set and --insecure is not), read by the Nous Portal
# plugin. Env HERMES_DASHBOARD_OAUTH_CLIENT_ID / HERMES_DASHBOARD_PORTAL_URL win when
# non-empty. Empty client_id = no provider; empty portal_url = production.
"oauth": {
"client_id": "", # agent:{instance_id} — Portal provisions this
"portal_url": "",
},
# Username/password gate (dashboard_auth/basic plugin, no OAuth IDP). Active when username
# plus password_hash (preferred) or password (hashed in-memory) are set; empty username =
# no-op. Env HERMES_DASHBOARD_BASIC_AUTH_USERNAME / _PASSWORD_HASH / _PASSWORD / _SECRET /
# _TTL_SECONDS win when non-empty. secret signs session tokens; empty = random per-process
# key (sessions die on restart, no multi-worker) — set 32+ random bytes. Hash:
# plugins.dashboard_auth.basic.hash_password('PW').
"basic_auth": {
"username": "",
"password_hash": "", # scrypt$...
"password": "",
"secret": "",
"session_ttl_seconds": 0, # 0 → plugin default (12h)
},
# Drain-control token auth (dashboard_auth/drain plugin). The secret is NOT here: env
# HERMES_DASHBOARD_DRAIN_SECRET; no-op unless >=256-bit, weak secrets rejected
# (fail-closed). scope = capability label; min_secret_chars in url-safe-b64 chars.
"drain_auth": {"scope": "drain", "min_secret_chars": 43},
# Public URL (env HERMES_DASHBOARD_PUBLIC_URL): full authority (scheme + host + optional
# prefix, e.g. https://example.com/hermes) for the OAuth redirect_uri; its hostname is
# trusted by Host/Origin guards and engages the auth gate when non-loopback. For proxies
# that don't forward X-Forwarded-Host/-Proto/-Prefix; X-Forwarded-Prefix is then IGNORED on
# the OAuth path. Empty or malformed (no http(s):// + host, or quote/angle/whitespace chars)
# = reconstruct from headers.
"public_url": "",
},
"privacy": {
"redact_pii": False, # hash user IDs and strip phone numbers from LLM context
},
# Text-to-speech. Each provider accepts an optional `max_text_length:` override for the
# per-request input-character cap; omit to use the provider's documented limit (OpenAI 4096, xAI
# 15000, MiniMax 10000, ElevenLabs 5k-40k model-aware, Gemini 32000, Edge 5000, Mistral 4000,
# NeuTTS/KittenTTS 2000).
"tts": {
# "edge" (free) | "elevenlabs" (premium) | "openai" | "xai" | "minimax" | "mistral" |
# "gemini" | "deepinfra" | "neutts" (local) | "kittentts" (local) | "piper" (local)
"provider": "edge",
# Seconds a local engine (Piper, KittenTTS) stays loaded after the last speech toggle
# turns off, so a quick re-activation (wake word, voice-chat restart) skips the reload.
# 0 unloads immediately.
"keep_warm_seconds": 60,
"streaming": {
# Shortest first sentence (chars) spoken on its own by streaming TTS; shorter openers
# ride with the next sentence. 20 suits English; CJK voice setups use ~6.
"min_len": 20,
},
"edge": {
# Popular: AriaNeural, JennyNeural, AndrewNeural, BrianNeural, SoniaNeural
"voice": "en-US-AriaNeural",
},
"elevenlabs": {
"voice_id": "pNInz6obpgDQGcFmaJgB", # Adam
"model_id": "eleven_multilingual_v2",
},
"openai": {
"model": "gpt-4o-mini-tts",
# gpt-4o-mini-tts voices: alloy, ash, ballad, cedar, coral, echo, fable, marin, nova,
# onyx, sage, shimmer, verse
"voice": "alloy",
# Forwarded verbatim in the request body for OpenAI-compatible servers whose cloned
# voices demand it (400 consent_required otherwise); "" sends nothing.
"consent_attestation": "",
# Raw PCM rate for streaming playback. OpenAI emits 24 kHz; a compatible endpoint that
# reports its rate (X-Audio-Sample-Rate header) overrides this automatically.
"pcm_sample_rate": 24000,
},
"gemini": {
"model": "gemini-2.5-flash-preview-tts",
"voice": "Kore",
# Gemini 3.1: aux-model rewrite inserts [audio tags] into the TTS script only.
"audio_tags": False,
# Optional local text file with performance direction; may include a `{transcript}`
# placeholder, else the live transcript is appended.
"persona_prompt_file": "",
},
"xai": {
"voice_id": "eve", # or a custom voice ID (docs.x.ai custom voices)
"language": "en", # BCP-47 code ("en", "pt-BR") or "auto"
"speed": 1.0, # 0.7–1.5
"auto_speech_tags": False, # insert expressive audio tags via LLM rewrite
"optimize_streaming_latency": 0, # 0–2, trades quality for lower latency
"sample_rate": 24000, # 22050 / 24000 / 44100 / 48000
"bit_rate": 128000, # MP3 bitrate; only applies when codec=mp3
},
"mistral": {
"model": "voxtral-mini-tts-2603",
"voice_id": "c69964a6-ab8b-4f8a-9465-ec0925096ec8", # Paul - Neutral
},
"minimax": {"model": "speech-02-hd", "voice_id": "English_expressive_narrator"},
"kittentts": {
"model": "KittenML/kitten-tts-nano-0.8-int8", # nano 25MB; micro 41MB; mini 80MB
"voice": "Jasper",
},
"neutts": {
"ref_audio": "", # path to reference voice audio (empty = bundled default)
"ref_text": "", # path to reference voice transcript (empty = bundled default)
"model": "neuphonic/neutts-air-q4-gguf", # HuggingFace model repo
"device": "cpu", # cpu, cuda, or mps
},
"piper": {
# Voice name (downloaded on first use) or absolute path to a .onnx file; list:
# github.com/OHF-Voice/piper1-gpl/blob/main/docs/VOICES.md. Optional keys: voices_dir
# (~/.hermes/cache/piper-voices/), use_cuda, length_scale (2.0 = twice as slow),
# noise_scale, noise_w_scale, volume, normalize_audio.
"voice": "en_US-lessac-medium",
},
"deepinfra": {
"model": "", # empty = first tts-tagged model from the live catalog
"voice": "default",
# optional "base_url" key overrides DEEPINFRA_BASE_URL for TTS only
},
},
"stt": {
"enabled": True,
# Echo the raw transcript of gateway voice messages back as a 🎙️ message.
"echo_transcripts": True,
# No seeded "provider": a stored value counts as an explicit user pick; unset = autodetect
# ladder. Valid: "local" (faster-whisper) | "groq" | "openai" | "mistral" | "elevenlabs" |
# "deepinfra". Global language hint unless a per-provider language overrides it. "en"
# because Whisper auto-detect misreads short/accented clips; "" = auto; or "es", "zh", ...
"language": "en",
# Client-side ffmpeg silence trim before cloud upload (local whisper uses VAD): silence
# inflates upload time, billing and hallucinations. Failure = raw upload.
"cloud_trim_silence": True,
"cloud_trim_threshold_db": -40, # quieter than this counts as silence
"cloud_trim_keep_ms": 300, # how much of each pause survives (natural pacing)
"local": {
"model": "base", # tiny, base, small, medium, large-v3
"language": "", # auto-detect; set "en", "es", ... to force
"initial_prompt": "",
# Anti-hallucination (faster-whisper decodes junk from silence). vad: Silero filter
# (false = raw audio, for music/ambient). A segment is dropped only if no_speech_prob
# ABOVE no_speech_prob_threshold AND avg_logprob BELOW logprob_threshold.
"vad": True,
"vad_min_silence_ms": 500, # min silence (ms) that splits speech chunks
"no_speech_prob_threshold": 0.6,
"logprob_threshold": -1.0,
"unload_after_idle_seconds": 0, # 0 = never; e.g. 300 frees the model after 5min
},
"groq": {
# whisper-large-v3, whisper-large-v3-turbo, distil-whisper-large-v3-en
"model": "whisper-large-v3-turbo",
"language": "", # auto-detect; set "en", "es", ... to force
},
"openai": {
# whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe, gpt-transcribe
"model": "whisper-1",
"language": "", # auto-detect; set "en", "es", ... to force
"timeout": 60, # seconds; allow self-hosted backends time to cold-start
"max_retries": 1, # OpenAI SDK transport retries
},
"mistral": {
"model": "voxtral-mini-latest", # voxtral-mini-latest, voxtral-mini-2602
"language": "", # auto-detect; set "en", "es", ... to force
},
"xai": {
"language": "", # auto-detect; set "en", "es", ... to force
},
"elevenlabs": {
"model_id": "scribe_v2", # scribe_v2, scribe_v1
"language_code": "", # auto-detect; set "eng", "spa", ... to force
"tag_audio_events": False,
"diarize": False,
},
"deepinfra": {
"model": "", # empty = first stt-tagged model from the live catalog
# optional "base_url" key overrides DEEPINFRA_BASE_URL for STT only
},
},
"voice": {
# How the Desktop voice conversation is wired:
# chained — STT → Hermes turn → TTS (the stt.* / tts.* providers below)
# gpt-live — one full-duplex voice model (OpenAI GPT-Live) owns the mic and speaker and
# DELEGATES every real request to Hermes (any model / provider you have
# selected); needs an OpenAI API key. $0.05/min voice layer billing.
"voice_chat_mode": "chained",
"gpt_live": {
"model": "gpt-live-1",
"voice": "marin", # marin | quartz | ripple | vesper | willow | stone | gleam | meridian | ...
# Extra sentences appended to the live model's conversation persona (tone, pacing, language).
"instructions": "",
# optional "api_key" / "base_url" keys override the OpenAI audio credentials for this mode only
},
"record_key": "ctrl+b",
"submit_mode": "direct", # TUI: direct submits immediately; draft = editable transcript
"max_recording_seconds": 120,
"auto_tts": False,
# Desktop remote clients call STT/TTS providers DIRECTLY (config + key fetched over
# authenticated REST at session start) instead of relaying via the gateway.
"client_direct": True,
"beep_enabled": True, # record start/stop beeps in CLI voice mode
"beep_volume": 0.3, # beep amplitude multiplier, 0.0-1.0
"thinking_sound": True, # ambient bubble sound while the agent works (volume = beep_volume)
"silence_threshold": 200, # RMS below this = silence (0-32767)
"silence_duration": 3.0, # seconds of silence before auto-stop
"barge_in": True, # interrupt the agent / stop TTS when the user starts talking
# Trip suppression after TTS onset (mic stays live the whole turn).
"barge_in_grace_seconds": 0.5,
# Speech trigger = quiet-room floor x this (floor calibrated BEFORE playback).
"barge_in_threshold_multiplier": 3.0,
# Saying EXACTLY one of these (case-insensitive, punctuation ignored) ends the voice chat
# instead of going to the agent. [] disables.
"stop_phrases": ["stop"],
},
# Native vision embeds (vision_analyze / browser screenshots on vision-capable main models) ride
# conversation history and are re-sent on every later API call.
"vision": {
# Byte budget for one embedded image (clamped 64 KiB..4 MiB). Raise it for dense phone
# screenshots of tables the model calls "unreadable" at 256 KB.
"embed_target_bytes": 256 * 1024,
# How often vision_analyze may embed the SAME image (region crops included) per session.
# null = 3 inside delegated subagents (they run unattended), unlimited for the main agent;
# an explicit number applies everywhere; 0 = unlimited.
"max_calls_per_image": None,
},
# "Hey Hermes" hands-free wake word: always-on, on-device hotword detection that starts a fresh
# voice session. Off by default; toggle with /wake.
"wake_word": {
"enabled": False,
"surface": "auto", # eligible surface: "auto" (first claimant) | "cli" | "tui" | "gui"
"input_device": None, # PortAudio input device index/name; null = process default
"capture": "auto", # auto | local | client (desktop streams mic via wake.feed)
# auto: first platform-supported engine (openwakeword, sherpa, porcupine).
# Explicit choices stay pinned. Porcupine needs PORCUPINE_ACCESS_KEY.
"provider": "auto",
# sherpa: this IS the detected phrase; other engines: cosmetic label (detection is keyed by
# the model/keyword below)
"phrase": "hey hermes",
"sensitivity": 0.6, # 0.0-1.0 threshold, consistent across engines (higher = stricter)
# openWakeWord/pyopen-wakeword only: consecutive over-threshold frames to fire (higher = fewer
# false triggers, more latency; 1 = single-frame)
"confirmation_frames": 3,
"start_new_session": True, # fresh session on wake vs. continue the current one
# sherpa only: listen for every wake-enabled profile's phrase and route to it
"profile_routing": True,
"openwakeword": {
# "hey_hermes" | built-in openWakeWord name ("hey_jarvis", "alexa", ...) | path to a
# custom .tflite model
"model": "hey_hermes",
},
"sherpa": {
# sherpa-onnx KWS model dir; empty = auto-download the small English zipformer
"model_dir": "",
},
"porcupine": {
# built-in keyword ("jarvis", "computer", ...) or path to a custom .ppn
"keyword": "jarvis",
},
},
"human_delay": {"mode": "off", "min_ms": 800, "max_ms": 2500},
# Context engine — how the context window is managed near the token limit. "compressor" =
# built-in lossy summarization; or a plugin name (e.g. "lcm") installed in
# plugins/context_engine/<name>/ or ~/.hermes/plugins/.
"context": {
"engine": "compressor",
# Return freed glibc pages at agent/TUI cleanup boundaries (no-op elsewhere).
"memory_trim": {
"enabled": True,
"cooldown_seconds": 60.0,
"log_every_n": 1, # INFO-log every Nth periodic trim; force paths always log.
# Suppress INFO logs when the readable RSS delta is smaller; 0 = log all.
"info_log_min_delta_mb": 0.0,
},
},
"memory": { # Persistent memory — bounded curated memory injected into the system prompt
"memory_enabled": True,
"user_profile_enabled": True,
# Approval gate for memory writes on BOTH foreground turns and the background review fork.
# true = foreground writes prompt inline; background writes are staged (/memory
# pending|approve <id>|reject <id>). To disable memory: memory_enabled.
"write_approval": False,
"memory_char_limit": 2200, # ~800 tokens at 2.75 chars/token
"user_char_limit": 1375, # ~500 tokens at 2.75 chars/token
# Periodic built-in memory review; 0 when an external provider auto-extracts.
"nudge_interval": 10,
# External memory provider plugin (empty = built-in only); only ONE at a time: "openviking",
# "mem0", "holographic", "retaindb", "byterover", or a catalog-installed one ("hindsight").
"provider": "",
},
# Subagent delegation — override the provider:model used by delegate_task so children run on a
# cheaper/faster model. Uses the same runtime provider resolution as CLI/gateway startup, so
# every configured provider is supported.
"delegation": {
"model": "", # e.g. "google/gemini-3-flash-preview" (empty = inherit parent)
"provider": "", # e.g. "openrouter" (empty = inherit parent provider + credentials)
# Fallback chain for delegated children (same entry format as the top-level list).
# For an unpinned child, null = inherit the parent chain; [] = disable fallback.
# A child pinned by provider, endpoint, or model gets no fallback unless this
# setting declares one explicitly.
"fallback_providers": None,
"base_url": "", # direct OpenAI-compatible endpoint for subagents
"api_key": "", # key for delegation.base_url (falls back to OPENAI_API_KEY)
# Wire protocol for delegation.base_url: "chat_completions" | "codex_responses" |
# "anthropic_messages". Empty = auto-detect from URL (e.g. /anthropic suffix); set
# explicitly for non-standard endpoints.
"api_mode": "",
# Per-child request settings on every delegation call (all resolution branches). Top-level
# keys = API kwargs (e.g. service_tier); "extra_body" sub-dict merges into extra_body, e.g.
# {"extra_body": {"provider": {"sort": "throughput"}}}. Explicit values win OVER
# runtime/parent overrides (extra_body deep-merged 1 level).
"request_overrides": {},
# compression_threshold_tokens: optional absolute cap on a subagent's compaction TRIGGER
# (not the request payload), applied as the lower of this and the child's ratio threshold.
# 0 (default) = no subagent-specific cap; children compact where the parent does — the lower
# of 0.50 x window and the global compression.threshold_tokens cap. A replay of a 1,393-agent
# run showed 200K-400K caps within 5% of each other in cost once cache prefixes are intact,
# and every compaction is a chance to lose detail, so the default stays off. A token count >= 16000 enables it;
# other values (true, "200k") are config errors: warned and ignored.
"compression_threshold_tokens": 0,
# When delegate_task narrows child toolsets, keep the parent's enabled MCP toolsets (so
# toolsets=["web"] doesn't strip MCP). false = strict intersection.
"inherit_mcp_toolsets": True,
# Per-subagent iteration cap (own budget, independent of the parent's).
"max_iterations": 250,
# Hard per-summary char ceiling on subagent results, layered on the dynamic budget (each
# summary is sized to the parent's remaining context headroom; trimmed text spills to
# ~/.hermes/cache/delegation/ with a head+tail window + read_file offset footer, nothing
# lost). 0 disables the ceiling; the dynamic budget still applies.
"max_summary_chars": 24000,
# Inactivity cap per child (seconds, floor 30) — time with NO progress, not total runtime. 0 = no cap:
# children fail only from real errors (API, tools, iteration budget). A progressing child (including one
# waiting on a multi-minute completion) restarts the window; a frozen one is caught.
"child_timeout_seconds": 0,
# Subagent effort: "ultra" | "max" | "xhigh" | "high" | "medium" | "low" | "minimal" |
# "none" (empty = inherit)
"reasoning_effort": "",
# Max parallel children per batch AND max concurrent background delegation units; async
# dispatches beyond it run synchronously. Floor 1, no ceiling.
"max_concurrent_children": 10,
# Background fan-outs return as ONE message when the whole call finishes. true = each task
# (or `group`) returns on its own as it finishes — more new turns for the orchestrator.
"independent_completions": False,
# Orchestrator role controls. Depth floored at 1, no ceiling; each level multiplies cost.
"max_spawn_depth": 1, # 1 = flat, 2 = orchestrator→leaf, 3+ = deeper
"orchestrator_enabled": True, # kill switch for role="orchestrator"
# Total subagents a finite one-shot run (hermes chat -q / --oneshot) may spawn; 0 = unlimited.
# Each child re-pays a cold system prompt and re-explores the repo, and one-shot spawns are mostly
# "review my own work" rather than parallel work (agent/oneshot_footprint.py).
"oneshot_max_children": 2,
# Subagent threads ALWAYS resolve approvals non-interactively (the parent TUI owns stdin;
# input() from a worker would deadlock). false = auto-deny, true = auto-approve "once"; both
# log a warning audit line. true only for trusted batch work.
"subagent_auto_approve": False,
# Subagent background processes (task_id "sa-...") route notify_on_complete / watch_pattern
# notifications to the PARENT; false suppresses them (the child's result is the
# deliverable). Async-delegation results are NEVER suppressed.
"surface_child_process_notifications": False,
},
# Ephemeral prefill messages file — JSON list of {role, content} dicts injected at the start of
# every API call for few-shot priming. Never saved to sessions/logs/trajectories.
"prefill_messages_file": "",
# Goals — persistent cross-turn /goal loop: after each turn an aux-model judge checks if the
# goal is satisfied, else a continuation prompt re-enters the session until done, budget
# exhausted, or paused. Judge failures fail OPEN; the budget is the backstop.
"goals": {
# Max continuation turns before auto-pause (/goal resume) — guards against judge false
# negatives and unbounded spend.
"max_turns": 20,
},
# Loops — /loop re-runs a prompt or slash command on a cadence in-session. Fixed interval fires
# on the user's clock; self-paced (no interval) starts at the floor and backs off exponentially
# while replies stop changing.
"loops": {
"min_interval_seconds": 30, # smallest fixed interval; tighter cadences raised to it
"max_ticks": 100, # auto-pause after this many wakeups unless --times set; 0 = unlimited
"self_paced_floor_seconds": 60, # Self-paced cadence bounds (seconds).
"self_paced_ceiling_seconds": 900,
},
# Mixture of Agents — named presets used by /moa. A preset is an execution mode around the main
# model, not a model itself: references + aggregator synthesize private guidance before each
# main-model iteration.
"moa": {
"default_preset": "default",
"active_preset": "",
# Write each MoA turn (reference + aggregator exact input/output/usage) as JSONL to
# <hermes_home>/moa-traces/<session_id>.jsonl (or trace_dir) for auditing.
"save_traces": False,
"trace_dir": "",
# PII/credential redaction of advisor outputs: "" off | "display" (UI reference blocks +
# traces only; aggregator sees raw) | "full" (also the aggregator prompt).
# Advisors can echo PII from the conversation (emails, formatted phone numbers) and credential
# shapes into reference blocks, traces, and the aggregator prompt. Modes ('' = off, the default):
# "display" — redact user-visible surfaces only (reference blocks shown in the UI + saved MoA trace
# records); the aggregator still sees raw advisor text. "full" — additionally redact the advisor
# text injected into the aggregator prompt (issue #59959).
"privacy_filter": "",
"presets": {
"default": {
"reference_models": [
{"provider": "openai-codex", "model": "gpt-5.5"},
{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"},
],
"aggregator": {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"},
"enabled": True,
}
},
},
# Skills — external skill directories shared across tools/agents. Paths are expanded (~, ${VAR})
# and resolved; read-only — creation goes to ~/.hermes/skills/ unless create_dir redirects it.
"skills": {
"external_dirs": [], # e.g. ["~/.agents/skills", "/shared/team-skills"]
# Where skill_manage-created skills go (empty = profile-local dir). When set, new skills
# land here AND agent-facing instructions name this path; expanded (~, ${VAR}), relative to
# HERMES_HOME, scanned alongside the local dir.
"create_dir": "",
# In a git checkout, <root>/.hermes/skills/ and <root>/.agents/skills/ load as the
# highest-precedence tier — ONLY if the root is in trusted_project_dirs. false = no scan, no
# untrusted-skills notice.
"project_discovery": True,
# Trusted project roots; managed by `hermes skills trust` / `untrust`.
"trusted_project_dirs": [],
# Skill names pinned as fully loaded in every new session (CLI, TUI, gateway, cron, API).
# Resolved once when the agent's prompt is first built; missing/disabled names warn and
# skip; HERMES_IGNORE_RULES suppresses the list like the other auto-injected context.
"auto_load": [],
# Substitute ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID} in SKILL.md content.
"template_vars": True,
# Pre-execute !`cmd` snippets in SKILL.md, inlining stdout (dates, git state...). Off:
# skill-author content would run on the host unapproved — trusted sources only.
"inline_shell": False,
"inline_shell_timeout": 10, # seconds per !`cmd` snippet
# Security-scan skills the agent writes via skill_manage. Off: the agent can run the same
# code via terminal() ungated, so it mostly blocks prose with risky keywords. On: a
# dangerous verdict is a tool error the agent can retry. Hub installs are always scanned.
"guard_agent_created": False,
# Advisory NVIDIA SkillEvaluator Tier 1 scan on `hermes skills install` (alongside the
# enforcing built-in guard), only if `skillevaluator` is on PATH (uv tool install
# "skillevaluator @ git+https://github.com/NVIDIA/SkillEvaluator.git"). Informational, never
# blocking; secrets-class findings shown red. No-op without it.
"tier1_advisory": True,
# Approval gate for skill_manage mutations on BOTH foreground turns and the background
# review fork. true = ALWAYS stage (SKILL.md too large for an inline prompt): /skills
# pending, /skills diff <id>, /skills approve|reject <id>.
"write_approval": False,
# Audit ledger: every skill mutation appends to ~/.hermes/skills/.curator_ledger.jsonl with
# before/after hashes (blobs under ~/.hermes/.curator_backups/blobs/); powers `hermes
# curator ledger` / `rollback <entry-id>`. Never a gate — failures can't block.
# See #79686.
"ledger": True,
# Size cap for that ledger: once the file grows past this, the next append rewrites it
# through the unchanged-file dedup and, if still over, drops the oldest entries (0 = keep
# the ledger append-only forever, the previous behaviour).
"ledger_max_bytes": 5 * 1024 * 1024,
},
# Curator — background maintenance of AGENT-CREATED skills (never hub-installed): marks
# long-unused skills stale, archives (never deletes) obsolete ones, optionally consolidates
# overlaps via a forked aux-model agent. Inactivity-triggered from session start, no cron
# daemon. `hermes curator status` shows the last run.
"curator": {
"enabled": True,
"interval_hours": 24 * 7, # hours between runs
"min_idle_hours": 2, # only run after the agent has been idle this long
"stale_after_days": 14, # mark "stale" after this many unused days
"archive_after_days": 30, # move to skills/.archive/ (recoverable) after this many
# LLM consolidation (umbrella-building) pass. OFF = deterministic inactivity prune only, no
# aux-model cost. `hermes curator run --consolidate` overrides once.
"consolidate": False,
# Also prune bundled built-ins (a suppression list stops `hermes update` restoring them);
# hub-installed skills are NEVER pruned. OFF by default: shipped skills vanishing from
# `skills_list` because nobody loaded them for 30 days surprised people (57 gone in one
# startup tick). true = built-ins age out like agent-created skills.
"prune_builtins": False,
# TTL purge of skills/.archive/: 0 = never; > 0 lets the explicit `hermes curator purge`
# delete older archived skills (never automatic; logged in the ledger).
"archive_ttl_days": 0,
# Before a consolidation pass (the only one that rewrites skill content in place), snapshot
# ~/.hermes/skills/ to ~/.hermes/skills/.curator_backups/<utc-iso>/skills.tar.gz (`hermes curator
# rollback`). The prune-only pass just moves directories into .archive/ and takes none.
"backup": {
"enabled": True,
"keep": 2, # retain last N regular snapshots
},
},
# Honcho AI-native memory — ~/.honcho/config.json is the source of truth (apiKey, workspace,
# peerName, sessions, enabled); hermes-specific overrides only here.
"honcho": {},
# IANA timezone (e.g. "Asia/Kolkata", "America/New_York"). Empty = server-local time.
"timezone": "",
"slack": {
"require_mention": True, # require @mention to respond in channels
"free_response_channels": "", # comma-separated channel IDs answered without mention
"allowed_channels": "", # if set, ONLY respond in these channel IDs (whitelist)
"require_mention_channels": "", # channel IDs where @mention is ALWAYS required
# Ignore messages whose first token @mentions another user unless the bot is also mentioned.
# Env: SLACK_IGNORE_OTHER_USER_MENTIONS.
"ignore_other_user_mentions": False,
"thread_require_mention": False, # require @mention in thread replies too
"channel_prompts": {}, # per-channel ephemeral system prompts
},
"discord": {
"require_mention": True, # require @mention to respond in server channels
"free_response_channels": "", # comma-separated channel IDs answered without mention
"allowed_channels": "", # if set, ONLY respond in these channel IDs (whitelist)
"auto_thread": True, # auto-create threads on @mention in channels (like Slack)
# Free-response channels reply inline by default; true also gives each top-level
# message in them its own thread (still mention-free). Env: DISCORD_FREE_RESPONSE_AUTO_THREAD.
"free_response_auto_thread": False,
"thread_require_mention": False, # require @mention in threads too (multi-bot threads)
# Bot authors must type @thisbot to trigger a reply; Discord reply pings alone do not count.
# Set False only for trusted legacy relays. Humans are unaffected.
"bots_require_inline_mention": True,
# Prepend recent channel scrollback when triggered (recovers messages gated out by
# require_mention); limit = max messages scanned.
"history_backfill": True,
"history_backfill_limit": 50,
# Replay messages missed while offline, after reconnect/startup.
"missed_message_backfill": {
"enabled": False,
"channels": "", # comma-separated channel IDs; empty uses free_response_channels
"window_seconds": 21600, # only inspect messages from the last 6 hours
"limit": 100, # global cap on messages scanned per reconnect
"max_dispatches": 10, # cap on recovered messages dispatched per reconnect
"max_attempts": 3, # lifetime re-dispatch cap for one message, whatever its outcome
},
"reactions": True, # add 👀/✅/❌ reactions to messages during processing
# Gateway transport health probe: inspects the WebSocket's ready/open/heartbeat state (never
# REST) as proof events still arrive. Any value 0 disables it.
"websocket_liveness_interval_seconds": 15,
"websocket_liveness_failure_threshold": 2,
"websocket_heartbeat_ack_max_age_seconds": 60,
"websocket_max_latency_seconds": 30,
# Dispatch-side dimension: a socket that ACKs heartbeats but delivers no events for this
# long is treated as deaf. 4 h absorbs a quiet server overnight; 0 disables it.
"websocket_event_max_silence_seconds": 14400,
# per-channel ephemeral system prompts (forum parents apply to child threads)
"channel_prompts": {},
# Opt-in DM role auth: DISCORD_ALLOWED_ROLES normally authorizes guild messages only (DMs
# need DISCORD_ALLOWED_USERS). A guild ID here also authorizes DMs from that guild's members
# holding the allowed role. Unset / "" / 0 = off.
# See #12136.
"dm_role_auth_guild": "",
# discord / discord_admin tools: allowed actions (comma string or YAML list; empty = all,
# subject to bot intents; unknown names dropped with a warning): list_guilds, server_info,
# list_channels, channel_info, list_roles, member_info, search_members, fetch_messages,
# list_pins, pin_message, unpin_message, create_thread, add_role, remove_role.
"server_actions": "",
# DEPRECATED no-op (uploads are always cached; messaging auth is the gate). Kept so existing
# configs don't error. Env: DISCORD_ALLOW_ANY_ATTACHMENT.
"allow_any_attachment": False,
# Max bytes per cached attachment (held in memory while written); 0 = no cap. Env:
# DISCORD_MAX_ATTACHMENT_BYTES.
"max_attachment_bytes": 33554432,
# Mention allowed users on approval prompts so owners notice them in shared channels. Env:
# DISCORD_APPROVAL_MENTIONS.
"approval_mentions": False,
# Voice-channel inactivity timeout (seconds); 0 = stay until `/voice leave`.
"voice_channel_inactivity_timeout_seconds": 300,
# Minimum seconds before force-stopping a VC playback; the adapter probes clip duration and
# extends this floor so long TTS isn't cut off.
"voice_playback_timeout_seconds": 120,
# Voice-channel software mixer (plugins/platforms/discord/voice_mixer.py): ambient
# "thinking" bed, verbal acks and TTS OVERLAP (ambient ducked) vs stop-and-swap.
"voice_fx": {
"enabled": False, # master switch for the mixer subsystem
"ambient_enabled": True, # play the idle "thinking" bed while tools run
"ambient_path": "", # custom loop audio file; "" = synthesised pad
"ambient_gain": 0.18, # idle bed loudness, 0.0–1.0
"duck_gain": 0.06, # ambient loudness while speech plays
"speech_gain": 1.0, # TTS / ack loudness, 0.0–1.0
"ack_enabled": True, # speak a short phrase before the first tool call
"ack_phrases": [ # picked at random; [] disables phrases
"Let me look into that.",
"One moment.",
"Checking on that now.",
"Give me a sec.",
"On it.",
],
},
},
"whatsapp": {
# reply_prefix: None = built-in "☤ *Hermes Agent*" header; "" disables; \n allowed.
},
"telegram": {
"reactions": False, # add 👀/✅/❌ reactions to messages during processing
# per-chat/topic ephemeral system prompts (topics inherit from parent group)
"channel_prompts": {},
"allowed_chats": "", # if set, ONLY respond in these group/supergroup chat IDs
"extra": {
# Bot API 10.1 native rich messages (tables/task lists/math). Off = legacy MarkdownV2,
# since rich messages are hard to copy as plain text.
"rich_messages": False,
# Experimental rich draft previews while streaming DMs; off because Telegram
# Desktop/macOS can overlay draft frames until the chat redraws.
"rich_drafts": False,
# CJK stays on legacy MarkdownV2 (Telegram Desktop/macOS garbles rich CJK, #47653);
# set True on an unaffected client to get native rich tables for CJK.
"allow_cjk_rich_messages": False,
},
},
"mattermost": {
"require_mention": True, # require @mention to respond in channels
"free_response_channels": "", # comma-separated channel IDs answered without mention
"allowed_channels": "", # if set, ONLY respond in these channel IDs (whitelist)
"channel_prompts": {}, # per-channel ephemeral system prompts
},
"matrix": {
"require_mention": True, # require @mention to respond in rooms
"free_response_rooms": "", # comma-separated room IDs answered without mention
"allowed_rooms": "", # if set, ONLY respond in these room IDs (whitelist)
},
# Approvals for dangerous commands.
# mode: manual (always prompt) | smart (aux LLM auto-approves low-risk) | off (= --yolo)
# cron_mode / single_query_mode / unattended_mode: deny | approve — what to do when a
# cron job, a -q session (HERMES_INTERACTIVE=1 but nobody to answer), or an unattended
# platform (webhook, msgraph_webhook, api_server; no /approve channel) hits one.
# deny blocks instantly so the agent finds another way instead of waiting out the
# timeout and failing closed.
# timeout: seconds before an unanswered prompt fails closed (CLI and gateway). 60s
# proved too tight for Telegram/Discord push notifications, hence 300.
"approvals": {
# single_query_mode — what to do when a single-query (-q) session hits a dangerous command. -q runs
# export HERMES_INTERACTIVE=1 (for interactive sudo prompts) but have NO user waiting to answer
# approval prompts — an unanswered prompt just waits the full timeout then fails closed, so the
# agent is forced to work around the block (often via execute_code). This setting makes that intent
# explicit: deny — block the command and let the agent find another way (default, safe; mirrors
# cron_mode deny) approve — auto-approve all dangerous commands in single-query mode These surfaces
# bind a session platform like chat gateways do, but have no send_exec_approval and no /approve
# channel — a pending approval there just blocks for the full timeout with nobody to answer (#37284,
# #87509): deny — block the command instantly and let the agent find another way (default, safe;
# mirrors cron_mode deny) approve — auto-approve all dangerous commands on unattended platforms
# Shared by the CLI prompt and gateway/messaging waits. Messaging approvals arrive as a push
# notification the user may not see immediately — 60s proved too tight on Telegram/Discord (the
# prompt expired before the user reached their phone), so the default is 300.
"mode": "smart",
"timeout": 300,
"cron_mode": "deny",
"single_query_mode": "deny",
"unattended_mode": "deny",
# Extra rules appended to the smart-approval guardian's SYSTEM prompt, e.g. "Always ESCALATE
# commands touching /etc".
"smart_policy": "",
# After this many consecutive guardian DENYs in a session, the deny message escalates to a
# hard-stop (report to user / ask for /approve). Approval resets; 0 off.
"denial_breaker_threshold": 3,
# Case-insensitive fnmatch globs against terminal commands; a match blocks even under --yolo
# / mode=off. Quote in YAML when starting with * or containing {}/!/: e.g. "git push
# --force*".
"deny": [],
# /reload-mcp confirms before rebuilding the MCP tool set (it invalidates the prompt cache,
# so the next message re-sends full input). "Always Approve" → false.
"mcp_reload_confirm": True,
# /clear, /new, /reset, /undo confirm before discarding state (Approve Once / Always Approve
# / Cancel via tools.slash_confirm; native buttons on Telegram/ Discord/Slack). "Always
# Approve" → false. HERMES_TUI_NO_CONFIRM=1 skips the TUI modal.
"destructive_slash_confirm": True,
},
# Permanently allowed dangerous command patterns (added via "always" approval).
"command_allowlist": [],
# User-defined quick commands that bypass the agent loop (type: exec only).
"quick_commands": {},
# Per-platform system-prompt hint overrides, keyed by platform name (whatsapp, slack, telegram,
# ...). Value: {"append": text} keeps the built-in hint and appends; {"replace": text}
# substitutes it; a bare string is shorthand for append. `replace` wins over `append` if both
# are given.
"platform_hints": {},
# Plugin system. `enabled`/`disabled` lists are written by `hermes plugins enable|disable` and
# deliberately omitted here so an empty default never clobbers a user allow-list.
"plugins": {
# Deadline (seconds) for one plugin Git clone, fetch or checkout. Slow repositories may
# need more time; each network operation is capped at one hour.
"clone_timeout_seconds": 300,
# Wall-clock cap (seconds) for one in-process Python plugin hook callback; shell hooks keep
# their own per-entry `timeout`. 0 = no cap (sync call on agent thread). Max 600.
"hook_callback_timeout": 30,
# Deadline (seconds) for one plugin's import + register() at load. A plugin that overruns it is
# skipped with the reason "load timed out" and the rest keep loading; the stuck worker thread is
# abandoned. 0 = no deadline (load inline). Max 600.
"load_timeout_seconds": 10,
# Keep loading external plugins that still import pre-decomposition module paths after the
# 2026-09-14 removal date (see COMPAT_MANIFEST.md, `hermes plugins compat`). Stopgap only: the
# old paths raise ImportError once the compat layer is actually removed.
"allow_deprecated_imports": False,
# Read-only plugin update-check cadence, hours (gateway tick; 0 disables). Applying stays
# explicit: `hermes plugins update <name>`, or auto_apply below (git-class plugins only,
# scan-gated by that same pipeline).
"auto_update_check_hours": 24,
# Opt-in unattended apply for the cadence check. Git-row plugins ONLY; every apply runs the
# same security scan / consent pipeline as the manual update command.
"auto_apply": False,
},
# Shell-script hooks: event name (pre_tool_call, post_tool_call, pre_llm_call, subagent_stop,
# ...) -> list of {matcher, command, timeout}. First run of a new command prompts for consent;
# approvals persist in ~/.hermes/shell-hooks-allowlist.json. Schema + examples:
# website/docs/user-guide/features/hooks.md.
"hooks": {},
# Auto-accept shell-hook registrations without a TTY prompt (also --accept-hooks or
# HERMES_ACCEPT_HOOKS=1). Gateway/cron/non-interactive runs need one of these to pick up
# newly-added hooks.
"hooks_auto_accept": False,
# Custom personalities: {"name": "system prompt"} or {"name": {"description", "system_prompt",
# "tone", "style"}}.
"personalities": {},
"auth": { # Login policy (credentials themselves live in auth.json / .env).
# Borrow and refresh the Codex CLI (~/.codex/auth.json) and Claude Code (~/.claude/.credentials.json)
# logins automatically when Hermes has no usable login of its own. Their refresh tokens are single-use
# and rotate, so two programs on one login can log each other out; set false to make Hermes use only
# its own logins (`hermes auth add <provider>`). `hermes auth add openai-codex` still offers the import
# interactively.
"adopt_external_logins": True,
# How `hermes auth add openai-codex` / `hermes model` sign in to OpenAI Codex.
# "device_code" (default): open a URL, enter a code. "browser": authorization-code + PKCE on
# the loopback listener http://localhost:1455/auth/callback (the redirect OpenAI registered
# for the Codex client) — for organizations that disable the device-code grant. Falls back
# to device code when that port is busy. `hermes auth add openai-codex --browser` opts in
# for one login without changing this key.
"codex_login_flow": "device_code",
},
"security": { # Security: pre-exec scanning via tirith plus related guards.
"allow_private_urls": False, # allow requests to private/internal IPs (OpenWrt, VPNs)
# CIDR blocks a local TUN proxy answers DNS with (Mihomo/Clash fake-ip, Surge enhanced).
# Answers inside these blocks are the proxy's sentinels, not internal hosts, so the guard
# dials them instead of rejecting them as private. Empty = normal private-address verdict.
"fake_ip_ranges": [],
"redact_secrets": True,
# Persisted acknowledgement for unattended model overrides whose tier lets the vendor train
# on prompts. The startup guard still warns every run; cost guards are unaffected.
"allow_data_training_tiers_noninteractive": False,
# Human-approval presentation transport. "builtin" = CLI/TUI/gateway/ACP surfaces; a plugin
# transport is used only when named explicitly. Transport timeout/error/invalid response
# DENIES unless transport_fallback is "builtin". Presentation only: plugins cannot detect,
# suppress, or auto-approve commands outside a correlated human response.
"approval": {"transport": "builtin", "transport_fallback": "deny"},
# Writes to agent-instruction files (AGENTS.md/CLAUDE.md/SOUL.md/.cursorrules, project-local
# .hermes config) always need human approval, even under yolo. Extra patterns are fnmatch
# globs on the basename (e.g. "*.mdc").
"protected_instruction_files": True,
"protected_instruction_extra_patterns": [],
"tirith_enabled": True,
"tirith_path": "tirith",
"tirith_timeout": 5,
"tirith_fail_open": True,
"website_blocklist": {"enabled": False, "domains": [], "shared_files": []},
# IDs of supply-chain advisories the user has read and acted on; acked ones stop the startup
# banner. Add via `hermes doctor --ack <id>`; remove by editing the list. Catalog:
# hermes_cli/security_advisories.py.
"acked_advisories": [],
# Lazy-install opt-in backend packages from PyPI when a backend that needs them is first
# enabled (e.g. `elevenlabs`). False = require explicit pip install for everything beyond
# the base set (restricted/audited/air-gapped environments).
"allow_lazy_installs": True,
},
"cron": {
"catch_up_missed": True, # False skips recurring misses beyond the local grace window.
# Let cron-spawned agents use the cronjob toolset (the "cron-librarian" pattern). Off by
# default: policy-denied in cron context to prevent unattended scheduling loops. Jobs
# created this way are user-owned in the same flat jobs table. Interactive toolsets
# (messaging/clarify) stay denied in cron regardless.
"allow_agent_scheduling": False,
# Pre-dispatch validation: before building any agent machinery, verify the provider API key
# resolves (unless a fallback chain exists), attached skills are ready, and delivery
# platforms are configured. Failure -> last_status=blocked_config, ONE alert, no LLM call.
# False = fail during the run instead.
"preflight": True,
# Default model for cron jobs (WHAT model runs). Fire-time resolution: per-job pin >
# cron.model > model.default (the main agent model). An unpinned job follows the main
# model on every run; cron.model decouples the whole fleet from chat. "" = fall through.
"model": "",
# Inference provider paired with cron.model (NOT the scheduler provider below). "" = resolve
# from global config.
"model_provider": "",
# Cron SCHEDULER provider (WHEN a due job fires). "" = built-in in-process 60s ticker. Name
# an installed provider (plugins/cron_providers/<name>/ or $HERMES_HOME/plugins/ <name>/),
# e.g. "chronos" (NAS-mediated managed cron for scale-to-zero). An unknown or unavailable
# provider falls back to the built-in so cron never loses its trigger.
"provider": "",
# Chronos settings; consulted only when provider == "chronos". All non-secret — the agent
# holds NO scheduler credentials (provision reuses the Nous Portal token).
"chronos": {
# NAS/portal base URL that arms/cancels one-shots and mints the inbound fire JWT (used
# as the expected issuer).
"portal_url": "https://portal.nousresearch.com",
# This agent's publicly reachable base URL; NAS POSTs {callback_url}/api/cron/fire. ""
# -> Chronos unavailable, resolver falls back to the built-in ticker.
"callback_url": "",
"expected_audience": "", # Expected JWT audience (e.g. "agent:{instance_id}").
# NAS JWKS URL for verifying the fire JWT signature. "" -> the fire endpoint refuses all
# tokens (never an unsigned decode).
"nas_jwks_url": "",
},
# Wrap delivered cron responses with a task-name header and "The agent cannot see this
# message" footer. False = clean output.
"wrap_response": True,
"delivery": { # Delivery behaviour for cron output sent through a live gateway adapter.
# Mark cron deliveries FINAL so the platform pushes them (Telegram's "important" mode
# otherwise sends with disable_notification=True and briefs look undelivered). False =
# silent, no-push deliveries.
"notify": True,
},
# Make cron deliveries CONTINUABLE (user can reply to a brief with it in context). False
# keeps deliveries isolated to the job's session; per-job `attach_to_session` overrides.
# Thread-capable platforms (Telegram topics, Discord/Slack threads) get a seeded thread per
# job via create_handoff_thread; DM-only platforms mirror the brief into the target DM
# session. Appended at a turn boundary via mirror_to_session, cached system prompt
# untouched. User-written bare platforms address home conversations, unlike `all`
# broadcast expansions, which do not gain mirror eligibility.
"mirror_delivery": False,
# Max due jobs run in parallel per tick. None/0 = unbounded (thread count only); 1 = serial.
# Env override: HERMES_CRON_MAX_PARALLEL.
"max_parallel_jobs": None,
# save_job_output keeps the N most recent .md files per job; 0 or negative disables pruning
# (for externally managed cleanup).
"output_retention": 50,
# Timeout (seconds) for a no-agent cron script. Env: HERMES_CRON_SCRIPT_TIMEOUT. Keep in
# sync with cron.scheduler._DEFAULT_SCRIPT_TIMEOUT.
"script_timeout_seconds": 3600,
# Timeout (seconds) for SessionDB() init inside cron jobs: state.db open/migrate has no
# timeout of its own against a wedged sqlite3.connect, and an unbounded hang wedges the
# job's dispatch guard forever. Env: HERMES_CRON_SESSION_DB_TIMEOUT. 0 = unlimited.
"session_db_timeout_seconds": 10,
# Timeout (seconds) per media attachment send during gateway delivery; large attachments
# (long TTS audio, big exports) need more than 30s. Env: HERMES_CRON_MEDIA_SEND_TIMEOUT.
# Keep in sync with cron.scheduler._DEFAULT_MEDIA_SEND_TIMEOUT.
"media_send_timeout_seconds": 300,
# Managed systemd gateway with no user session (containers, no linger): false runs
# cron jobs as a direct external subprocess (warns once; no cgroup isolation), true
# fails closed with the enable-linger remedy. Kanban always requires a scope.
"require_restart_safe_scope": False,
# A job failing with the SAME error alerts once, then stays silent for this many hours
# before one reminder ping (the run is still recorded; `hermes cron incidents` shows it).
# A green run or a different error alerts again immediately; `hermes cron incidents ack`
# silences a signature for good. 0 = re-alert on every failing run. Keep in sync with
# cron.scheduler.DEFAULT_FAILURE_REPEAT_ALERT_HOURS.
"failure_repeat_alert_hours": 6,
},
# Kanban multi-agent coordination. The dispatcher ticks every N seconds, reclaims stale claims,
# promotes dependency-satisfied todos to ready, and fires `hermes -p <assignee> chat -q ...` per
# claimable task. Run ONE dispatcher per profile; two on the same kanban.db race for claims.
"kanban": {
# Auto-subscribe the originating gateway/TUI session to completion + block events when
# kanban_create is called from a session with a persistent delivery channel. Disable for
# profiles that prefer explicit kanban_notify-subscribe calls per task.
"auto_subscribe_on_create": True,
# Poll and deliver Kanban subscriptions from this gateway. Disable on profiles that do
# not own notification subscriptions to avoid an idle five-second board probe.
"notify_in_gateway": True,
# Run the dispatcher inside the gateway process (~300µs per idle tick). False only if you
# run it as a separate unit or don't want the gateway spawning workers.
"dispatch_in_gateway": True,
# Auto-claim tasks in the review column and spawn the assigned profile with the bundled
# sdlc-review skill. Disable where every review is done manually from the dashboard.
"review_dispatch": True,
# Seconds between dispatcher ticks. Lower = snappier pickup; higher = less SQL pressure.
"dispatch_interval_seconds": 60,
# Auto-block after this many consecutive non-success attempts (spawn_failed, timed_out,
# crashed) for the same task/profile. Reassignment resets the streak.
"failure_limit": 2,
# Worker stdout/stderr log rotation at spawn time (2 MiB + one backup). Raise to keep more
# early failure evidence from long-running workers.
"worker_log_rotate_bytes": 2 * 1024 * 1024,
"worker_log_backup_count": 1,
# Profile for the root/orchestration task after Triage decomposition; "" = default profile.
# Does not control the decomposer LLM path (see auxiliary.kanban_decomposer).
"orchestrator_profile": "",
# Assignee when the orchestrator can't match one to an installed profile; "" = default
# profile. A task never ends up with assignee=None.
"default_assignee": "",
# Global cap: positive int = the HOST never has more than N tasks 'running' across all
# boards and both dispatch lanes. None = ~MemTotal / 512 MiB clamped to [2, 8]; where
# MemTotal is unreadable (macOS/Windows) None means no cap.
# Global concurrency cap (#33488): when set to a positive int, the HOST never has more than N tasks
# in 'running' at once — counted across every active board and across both the ready and review
# dispatch lanes (workers are OS processes sharing one machine's memory, so the cap bounds the
# machine, not each board; OOF-30). Unset (None) means "derive from system memory" (OOF-30/OOF-77):
# the dispatcher caps concurrency at roughly MemTotal / 512 MiB, clamped to [2, 8] — e.g. 2 workers
# on a 1 GiB VM. On hosts where total memory can't be read (macOS/Windows), unset falls back to no
# cap. Set an explicit value to override the derived default in either direction.
"max_in_progress": None,
# Per-profile cap: positive int = no single profile runs more than N workers even if the
# global caps allow; blocked tasks defer to the next tick. None = no per-profile cap. Useful
# when fan-out would saturate one profile's model/API quota/browser pool.
# Unset (None) means "no per-profile cap" — backward-compatible with existing installs. Useful for
# fan-out workflows that would otherwise saturate one profile's local model / API quota / browser
# pool while leaving other profiles idle. See #21582.
"max_in_progress_per_profile": None,
# Per-home claim allowlist for boards shared across Hermes homes (#110995): profile names
# this home's dispatcher may claim (list or comma-separated string). None = any existing
# profile is claimable. Set = fail-closed (an empty list claims nothing). Every home has a
# root profile named "default", so on a shared kanban.db every home can otherwise claim
# default-assigned cards.
"dispatch_profiles": None,
# Auto-run the decomposer on Triage tasks every tick. False = manual via `hermes kanban
# decompose <id>` or the dashboard's Decompose button.
"auto_decompose": True,
# Max triage tasks decomposed per tick, bounding the aux-LLM burst from a bulk load. Excess
# defers to the next tick.
"auto_decompose_per_tick": 3,
# Running tasks with no heartbeat (last_heartbeat_at) for this many seconds are reclaimed to
# ready on the next tick; a still-running local worker is terminated first. 0 = off.
"dispatch_stale_timeout_seconds": 14400,
# Each tick, requeue 'running' cards with broken claim bookkeeping (claim_lock or
# claim_expires NULL with a dead worker) that TTL/crash/stale recovery can't see. False
# keeps orphans frozen for manual forensics.
"reconcile_orphans": True,
# Notify subscriptions survive `done` (completion is reversible) and are removed on archive.
# On boards that never archive, the notifier GC purges subscriptions for tasks done with no
# activity for this many days so stale rows aren't scanned forever. 0 = off.
"done_sub_retention_days": 30,
},
# Bot Mode cross-connection relay (tools/bot_relay.py): envelopes queued by message_agent for
# agents on other connections wait in an on-disk outbox until the Desktop drains them.
"bot_mode": {
# Drain-time TTL (seconds): older envelopes are NOT delivered on drain; the sender gets an
# error reply (reason 'queued_expired') so a DM can't land hours late as a zombie. 0 = no
# drain-time expiry (the 6h stale-artifact sweep still applies).
"envelope_ttl_seconds": 900,
# How long a second delivery into a busy target profile queues behind the current turn
# before failing with a structured 'target_busy' error. Deliveries are serialized per
# profile with a cross-process file lock.
"turn_wait_seconds": 120,
},
"code_execution": { # execute_code settings (programmatic tool calls).
# project = run in the session cwd with the active venv/conda python so project deps and
# relative paths resolve. strict = isolated temp dir with hermes-agent's own python
# (sys.executable): max isolation, project deps/relative paths won't work. Env scrubbing
# (*_API_KEY, *_TOKEN, *_SECRET, ...) and the tool whitelist apply in both modes.
"mode": "project",
# Session kernels are always on locally (`kernel_mode` is ignored) and remotely
# (tools/code_kernel_remote.py; a backend that cannot spawn a kernel fails open to
# per-call). One kernel per (session owner, mode, interpreter, cwd, tool-set) keeps state
# across calls and turns; subagents get their own. Kernels die with the session, after
# kernel_idle_timeout idle seconds, or by LRU eviction past max_session_kernels. A
# timed-out/interrupted cell kills the kernel; env is frozen at spawn (reset=true after
# changing passthrough). Tool RPC authority (approval, session, allow-list, call budget) is
# rebound per cell — that runtime boundary is the cross-cell enforcement.
"kernel_idle_timeout": 1800,
"max_session_kernels": 4,
},
# Tool Search replaces deferred tools in the model-facing array with the
# tool_search / tool_describe / tool_call bridges and surfaces them on demand.
# Working-set core tools stay eager, while the explicit ``defer`` list below
# may include cold, event-triggered built-ins as well as plugin/MCP tools.
"tools": {
"tool_search": {
# Tiered: tier 0 (no deferred tools) = everything eager; tier 1 = bridge + a
# name+description manifest when it fits the budget (degrades to names-only); tier 2
# (over budget even names-only, e.g. ~3,300-tool APIs) = bare bridge + a
# one-line-per-server summary (name + tool count). "auto"|"on" = activate when at least
# one deferred tool exists ("auto" is an alias of "on" today, reserved for a future
# budget-gated mode; keep it the default so explicit "on"/"off" pins are unaffected).
# "off" = pass-through, no bridge.
"enabled": "auto",
# Listing budget as % of the model's context length; effective budget = min(this % of
# context, listing_max_tokens). Range 0..100.
"threshold_pct": 5,
# Hits per query when the model omits `limit`. Range 1..max_search_limit.
"search_default_limit": 5,
# Hard upper bound the model may request via `limit` (per query). Range 1..50.
"max_search_limit": 25,
# Catalog listing embedded in the bridge description (name + first sentence ≤60 chars,
# grouped by server/toolset). "auto" = include when it fits (falls back to names-only,
# then bare tier-2 bridge); "on" = same rendering, explicit intent; "off" = always the
# bare bridge.
"listing": "auto",
# Absolute cap on the embedded listing in tokens (chars/4), regardless of context size.
# Range 200..60000.
"listing_max_tokens": 4000,
# Tools replaced by the bridge by default. This list intentionally includes cold,
# event-triggered built-ins; an explicit list replaces it wholesale and [] keeps every
# tool eager. The runtime fallback in tools/tool_search.py derives from this value.
"defer": [
"computer_use", "session_search", "image_generate",
"todo_list", "process_manage", "cronjob_manage",
# Desktop GUI surface (desktop_ui + project toolsets)
"drive_preview", "gui_tour", "desktop_preview", "annotate_preview",
"show_tip", "desktop_project", "close_terminal",
"apply_layout", "read_terminal", "read_window_below", "focus_pane",
],
},
# Remote connector discovery/lifecycle through the Nous tool gateway.
# The flag is the user's off switch; availability additionally requires
# the portal sign-in every managed tool gates on.
"connectors": {"enabled": True},
},
"logging": { # File logging to ~/.hermes/logs/: agent.log captures INFO+, errors.log WARNING+.
"level": "INFO", # minimum level for agent.log: DEBUG, INFO, WARNING
"max_size_mb": 5, # max size per log file before rotation
"backup_count": 3, # rotated backups to keep
},
# Remote model-catalog manifest: curated OpenRouter / Nous Portal model lists fetched from this
# URL (falls back to the in-repo snapshot on network failure), so picker lists update without a
# release. Default URL is served by the docs-site GitHub Pages deploy.
"model_catalog": {
"enabled": True,
"url": "https://hermes-agent.nousresearch.com/docs/api/model-catalog.json",
# Disk cache TTL in minutes. The gateway refreshes in the background on this cadence; the
# CLI refetches on the next /model or `hermes model` once the cache is older. Network
# failures silently use the stale cache. Legacy `ttl_hours` is honoured if set.
"ttl_minutes": 20,
# Per-provider override URLs for self-hosted curation lists using the same schema, e.g.
# providers: {openrouter: {url: https://example.com/my-curation.json}}.
"providers": {},
},
# Per-model metadata overrides. Fields: context_window, supports_tools,
# supports_vision, supports_reasoning, model_family. <provider>.<model_id> wins over
# models.dev/OpenRouter/hardcoded defaults for the fields it sets (chain order in
# agent/model_metadata.py). <provider>._default and top-level _default fill gaps ONLY for models
# the catalog does not know, so they never clamp known models. Unknown ids start from safe
# defaults (200K context, tools on) and get patched; supports_vision / supports_reasoning stay
# UNKNOWN (fail-open) unless the override sets them — a context_window-only entry must not turn
# into "text-only" and hide vision_analyze / reasoning controls (#112649). Provider keys: Hermes
# or models.dev id; model ids match case-insensitively. Example: {"custom:my-local-vllm":
# {"my-llava-model": {"context_window": 8192}}}
# Semantics: 1. NOTE: an explicit model.context_length (global) and a custom_providers per-model
# context_length are user settings at other layers and are consulted in the resolution chain order
# documented in agent/model_metadata.py. 2. See #84482, #8731.
"model_overrides": {},
# models.dev registry (context windows, capabilities, pricing, modalities): fetched on startup,
# served from cache, refreshed by a background daemon with ETag conditional GET. Override `url`
# to point at a mirror (e.g. behind a corporate proxy).
"models_dev": {
"url": "", # empty = default https://models.dev/api.json
},
"network": {
# Force IPv4. With broken/unreachable IPv6, Python tries AAAA first and hangs for the full
# TCP timeout before falling back. True skips IPv6 entirely.
"force_ipv4": False,
},
# Gateway monitoring: service health + redacted operational diagnostics exported over OTLP to an
# operator endpoint (OTEL Collector, DataDog, ...). Content-free by construction: no prompts,
# messages, tool args/results, session history, usage analytics, audit logs, or trajectories.
# Nothing is collected or sent until enabled with an endpoint.
"monitoring": {
# Stable install identifier on exported signals so operators can tell instances apart. "" =
# mint a fresh UUID on first use; clear it to rotate. Carries no account identity.
"install_id": "",
"gateway_health_export": { # Gateway health & diagnostics export.
"enabled": False,
"metrics_enabled": True,
"diagnostic_events_enabled": True,
"warning_error_events_enabled": True,
"export_interval_seconds": 60,
"logs_export_interval_seconds": 5,
"resource_attributes": {
"service.name": "hermes-gateway", "deployment.environment.name": "production"
},
},
# OTLP destination. headers_env maps header names to ENVIRONMENT VARIABLE NAMES (never
# secret values); values are read from the environment at export time.
"export": {"otlp": {"enabled": False, "endpoint": "", "headers_env": {}}},
},
"gateway": { # Gateway settings (messaging platforms: Telegram, Discord, Slack, ...).
# Seconds to let a SIGTERM-interrupted gateway agent unwind before adapter/database
# teardown. Keep short so service-manager shutdowns don't exhaust their stop budget.
"signal_interrupt_grace_timeout": 1,
# Durable delivery-obligation ledger: final responses are recorded in state.db around the
# platform send; a gateway that died between finalize and platform ACK redelivers on next
# boot (ambiguous cases carry a "recovered reply — may be a duplicate" marker;
# at-least-once). Disable to lose in-flight final responses on crash/restart.
"delivery_ledger": True,
# Seconds to wait for one platform to connect at startup/reconnect; raise on "discord
# connect timed out" loops (many slash commands to sync). 0/negative = wait forever. Bridged
# to HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT, which wins if set explicitly.
# Seconds the gateway waits for a single messaging platform to finish connecting during startup (and
# on reconnect). Discord in particular can blow past the old fixed 30s when an account has many
# slash commands to sync (#19776: 90-173 skills → ~28-31s sync). Raise this if your gateway hits
# "discord connect timed out" / "Timeout waiting for connection to Discord" restart loops. ``0`` or
# negative disables the timeout entirely (wait indefinitely).
"platform_connect_timeout": 30,
# Event-loop liveness watchdog: a daemon thread probes the asyncio loop; after consecutive
# missed probes it dumps all-thread stacks and hard-exits with the service-restart code so
# systemd/launchd revives the process instead of leaving a wedged-but-alive zombie.
# Set to false to disable. See #69089.
"loop_watchdog": True,
# Watchdog tuning (defaults mirror gateway/shutdown_watchdog.py): probe_interval = seconds
# between probes; probe_timeout = seconds before an unprocessed probe counts as a miss;
# max_strikes = consecutive misses before hard-exit 75 (~90-120s of sustained loop block at
# the defaults).
"loop_watchdog_probe_interval_s": 30.0,
"loop_watchdog_probe_timeout_s": 10.0,
"loop_watchdog_max_strikes": 3,
# Allow all users without allowlists (security opt-in).
"allow_all_users": False,
# Bot-to-bot loop guard: admitted bot messages per conversation before a cooldown.
"bot_loop_guard": {"enabled": True, "max_events": 20, "window_seconds": 300, "cooldown_seconds": 600},
# Startup-liveness watchdog: stdlib-only daemon thread armed at process entry that
# hard-exits 75 if the loop isn't live within the deadline. Armed before config loads, so
# run_gateway() bridges these to HERMES_STARTUP_WATCHDOG / HERMES_STARTUP_WATCHDOG_TIMEOUT_S
# and re-arms the live handle; explicit env wins.
"startup_watchdog": True,
"startup_watchdog_timeout_seconds": 300,
# Keep writing the legacy ~/.hermes/sessions/sessions.json mirror of the routing index
# (primary copy: state.db gateway_routing table). True for external tooling and downgrade
# safety; False stops producing the file.
"write_sessions_json": True,
# One gateway for every profile on this host: the DEFAULT profile's gateway also connects
# each named profile's bots (their own .env / config.yaml, per-profile secret scope) and
# stamps the profile into session keys. This is the ONLY supported topology — there is no
# `false` opt-out any more: an explicit `false` still parses (it is the runtime mode flag
# every scoped code path reads) but is warned about and IGNORED for process topology, and
# `hermes gateway migrate --multiplex` folds any per-profile fleet that is left.
# An UNSET key is a request, not a verdict: at boot the gateway runs the migration
# preflight and stays standalone (logging why) while a secondary still runs its own
# gateway or a blocker exists, then converges once that is resolved.
# TWO things DO change on a host that had pinned `false`, and neither is a process:
# • INGRESS — `/p/<profile>/` on the default listener goes 404 -> served
# (gateway/api_server.py::_resolve_request_profile, gateway/webhook.py). A host that
# opted out GAINS that HTTP surface; it is authenticated exactly like the default
# profile's, but it is new reachable surface, so audit any reverse proxy that assumed
# /p/ was dead.
# • SECRET SCOPE — eager multi-profile activation no longer consults the flag
# (tui_gateway/launch_profile_policy.py), so a host with a leftover servable profile
# dir flips eager=false/reads-open -> eager=true/fail-closed: an UNSCOPED `get_secret`
# now raises UnscopedSecretError, and a key that lives ONLY in the unit's
# `Environment=` (no .env) disappears from file-built scopes. A genuinely
# single-profile host never activates and is byte-identical.
# Two profiles configuring the same bot token cannot be served together — the duplicate
# adapter is parked; `hermes profile create --clone` therefore leaves messaging channels
# behind unless --clone-channels is passed.
"multiplex_profiles": True,
# May `hermes update` fold this install onto a multiplexed default gateway by itself?
# True (the default) keeps today's behaviour: a multi-profile install whose secondaries run
# their own gateways is migrated automatically after an update when nothing blocks it.
# Set to False to choose WHEN you converge, not whether: the fold is left to you to run by
# hand (it is not an opt-out from the one-gateway-per-host model, which has none). Only the
# AUTOMATIC path reads this: `hermes gateway migrate --multiplex` is explicit and proceeds.
"auto_multiplex_migration": True,
# Route inbound chats of the default profile's bots to another profile
# (gateway/profile_routing.py): [{profile, platform, chat_id|user_id|guild_id|...}].
# Most-specific match wins; only read by the multiplexing default gateway.
"profile_routes": [],
# Scale-to-zero idle TIMEOUT only. When an instance is opted in via the NAS "Labs" toggle
# (HERMES_SCALE_TO_ZERO env stamp) AND messaging is relay-only/absent AND a wakeUrl is
# registered, the relay transport goes dormant so the platform (e.g. Fly autostop) can
# suspend the machine; it wakes on the wakeUrl poke. Enablement is the Labs toggle, never a
# config key. 0/negative = default.
"scale_to_zero": {"idle_timeout_minutes": 2},
# Auto-resume restart-loop breaker. A supervisor-revived gateway auto-resumes the
# SIGTERM-interrupted session; if that turn keeps triggering the kill, boots no more than
# `max_gap_seconds` apart (floored by `window_seconds`) chain, and after `max_restarts`
# auto-resume is SKIPPED for that boot (inbound messages still served). Gap-based chaining
# also catches SLOW ~150s crash cycles. max_restarts=0 disables.
"restart_loop_guard": {"max_restarts": 3, "window_seconds": 60, "max_gap_seconds": 300},
# Respawn-storm circuit breaker (complements restart_loop_guard): counts (re)starts in a
# sliding window and sleeps an exponential backoff before booting so a crash-looping
# supervisor can't hammer the process. max_starts <= 0 disables. Env escape hatches:
# HERMES_GATEWAY_MAX_STARTS / HERMES_GATEWAY_START_WINDOW_S.
"respawn_storm": {"max_starts": 5, "window_seconds": 120},
# Prefix user messages IN THE MODEL'S CONTEXT with a timestamp (e.g. "[Tue 2026-04-28
# 13:40:53 CEST]") for temporal awareness. Persisted transcripts stay clean (timestamp is
# message metadata regardless), so enabling later surfaces past send-times too.
"message_timestamps": {"enabled": False},
# Max bytes of inbound image/audio/video the gateway buffers into RAM and caches to disk.
# Media is read fully into memory first, so unbounded uploads (Discord Nitro: 500 MB) or
# huge remote URLs can OOM-kill constrained deployments. Enforced in
# gateway/platforms/base.py for every adapter. 0 = no cap. Default 128 MiB.
"max_inbound_media_bytes": 134217728,
# Let adapters read HTTP_PROXY/HTTPS_PROXY/NO_PROXY/SSL_CERT_FILE from the environment and
# auto-detect generic/macOS system proxies. False when the gateway inherits a proxy it must
# not use (e.g. a scheduled task picking up a Clash/V2Ray HTTP_PROXY -> "Cannot connect to
# host 127.0.0.1:7890"). Per-platform vars (DISCORD_PROXY, TELEGRAM_PROXY, ...) are still
# honored.
"trust_env": True,
# Media delivery. False: any emitted file path is delivered natively unless under the
# credential/system denylist (/etc, /proc, ~/.ssh, ~/.aws, ~/.hermes/.env, auth.json). True:
# files must be under the Hermes cache, media_delivery_allow_dirs, or fresher than
# trust_recent_files_seconds — recommended for public-facing gateways so prompt injection
# can't exfiltrate host secrets. Bridged to HERMES_MEDIA_DELIVERY_STRICT.
"strict": False,
# Extra roots (project/scratch dirs, mounted shares) from which bare file paths may be
# uploaded; the Hermes cache is always trusted. List of absolute paths or one
# os.pathsep-separated string; tildes expanded. Bridged to HERMES_MEDIA_ALLOW_DIRS. Honored
# in both modes.
"media_delivery_allow_dirs": [],
# Trust files whose mtime is within trust_recent_files_seconds even outside the cache/
# allowlist (e.g. `pandoc -o /tmp/report.pdf`); system paths stay blocked. False =
# pure-allowlist mode. Bridged to HERMES_MEDIA_TRUST_RECENT_FILES. Only consulted when
# strict is true.
"trust_recent_files": True,
# Recency window in seconds; 600 covers a multi-tool turn. Bridged to
# HERMES_MEDIA_TRUST_RECENT_SECONDS. Only consulted when strict is true.
"trust_recent_files_seconds": 600,
"api_server": { # OpenAI-compatible API server platform (gateway/platforms/api_server.py).
# Max concurrent agent runs. Requests to /v1/chat/completions, /v1/responses, and
# /v1/runs beyond this get HTTP 429 + Retry-After, bounding CPU/memory/LLM-quota
# exhaustion from a request flood. 0 = no cap.
"max_concurrent_runs": 10,
# Cap (chars) on each tool output and tool-call argument string in the stored
# /v1/responses conversation history used for previous_response_id chaining. The
# stored history is cumulative, so a few large tool outputs can make one
# response_store.db write several hundred KB. 0 = store tool outputs verbatim
# (default: the capped text is what the model is replayed on the next turn).
"history_tool_output_max_chars": 0,
},
},
# Real-time token streaming to messaging platforms (gateway; restart after enabling). Off by
# default: costs extra edit/draft API calls per response.
"streaming": {
"enabled": False, # When false, each response is delivered as one final message.
# auto = native drafts where supported (Telegram DMs, Bot API 9.5+), edits elsewhere
# (Discord, Slack, Matrix, Telegram groups); draft = drafts with edit fallback; edit =
# progressive editMessageText only; off = disabled.
"transport": "auto",
# Minimum seconds between progressive edits (Telegram's ~1 edit/s flood envelope).
"edit_interval": 0.8,
# Flush the buffer once this many chars accumulate, so short replies feel instant.
"buffer_threshold": 24,
"cursor": " \u2589", # Cursor glyph appended to the in-progress message.
# Telegram only: when >0, if the preview was visible at least this many seconds the final
# edit is sent as a fresh message so the timestamp reflects completion.
"fresh_final_after_seconds": 0.0,
},
# Automatic cleanup of ~/.hermes/state.db, which otherwise grows without bound and slows FTS5
# inserts, /resume listing, and insights queries.
"sessions": {
# Prune ENDED sessions inactive for retention_days (activity = freshest of live activity /
# latest message / creation) about once per min_interval_hours at startup. Open, pinned, or mid-turn sessions
# are never deleted; stale automation sessions whose process died are *closed*, then get a
# full retention window before removal.
"auto_prune": True,
# Inactive days of ended-session history to keep (= `hermes sessions prune`).
# When true, prune ENDED sessions inactive for retention_days once per (roughly) min_interval_hours
# at CLI/gateway/cron startup. Activity is the freshest of live activity (last_activity_at) / latest
# message timestamp / creation time. Sessions that are still open, pinned, or mid-turn are never deleted — the
# only open rows the sweep touches are stale automation sessions (cron/kanban/subagent/one-shot CLI)
# whose process died without closing them; those are *closed*, not deleted, and get a further full
# retention window before removal. Default true since #54189: without it state.db grows without
# bound (multi-GB installs reported within weeks).
"retention_days": 90,
# Auto-archive (soft-hide, never delete) sessions with no activity for auto_archive_days,
# once per min_interval_hours. Pinned sessions are exempt.
"auto_archive": False,
# Idle days before auto-archive hides a session (only when auto_archive is true).
"auto_archive_days": 3,
# List delegate_task subagent runs in session lists (desktop sidebar, dashboard, session.list),
# nested under their parent. Off by default: they are machinery, not conversations.
"show_subagents": False,
# VACUUM after a prune that deleted rows (SQLite never reclaims disk on DELETE). VACUUM
# blocks writes (~seconds per 100MB), so it runs only at startup, only when ≥1 session was
# deleted AND freelist/page_count > 25%.
# SQLite does not reclaim disk space on DELETE — freed pages are just reused on subsequent INSERTs —
# so without VACUUM the file stays bloated even after pruning. See #54189.
"vacuum_after_prune": True,
# Minimum days between VACUUM rewrites; pruning keeps its normal cadence.
"min_vacuum_interval_days": 30,
# Minimum hours between auto-maintenance runs (tracked in state.db state_meta, shared across
# processes).
"min_interval_hours": 24,
# Notice about the compact FTS layout (reclaims ~60%+ of state.db). OPT-IN: legacy indexes
# stay until `hermes sessions optimize-storage` runs, since the rebuild is disk-heavy on
# large DBs. advise = `hermes update` prints a one-line notice with reclaimable size when a
# legacy index is detected; require = shown as a REQUIRED upgrade (tooling may gate on it);
# off = none.
"fts_optimize_notice": "advise",
# CJK-bigram search index (messages_fts_cjk). When the extension is built
# (native/fts5_cjk/build.sh → ~/.hermes/lib/libfts5_cjk.so), 1-2 char CJK terms get exact
# index matches instead of LIKE scans. True = use when present (inert otherwise); False =
# never load/serve it. Bridged to HERMES_CJK_FTS.
"cjk_fts": True,
# Slow session-search threshold (ms): searches at/above it log one INFO line with the
# routing path (fts_cjk / fts5 / trigram / like_scan). 0 logs every search. Bridged to
# HERMES_SEARCH_SLOW_MS.
"search_slow_ms": 1000,
# Transcript guards (a runaway 100k+ row session can exhaust memory when materialized at
# once; 0 disables). Max active messages (across the compression lineage) for interactive
# resume.
"max_resume_messages": 20000,
# Max active messages per session for in-memory export (`hermes sessions export`); checked
# per session, so full-DB backups of small sessions work.
"max_export_messages": 20000,
},
# First-touch onboarding hints (agent/onboarding.py). Each hint shows once and is latched under
# `seen`; wipe the section to re-see all hints.
"onboarding": {
"seen": {},
# First-ever gateway message: ask = offer to build a user profile (consent- gated; never
# reads connected accounts silently); off = plain intro only.
"profile_build": "ask",
},
# Privacy-safe aggregate metrics in this profile's local telemetry dir. Collection (`enabled`)
# and transmission to Nous (`send`) are SEPARATE opt-ins; see
# website/docs/developer-guide/relay-shared-metrics.md Appendix A for consent/retention.
"telemetry": {
"shared_metrics": {
"enabled": False,
# Requires `enabled` (`send` alone logs an error). A package is sent only if its whole
# period is inside a recorded consent window.
"send": False,
# Ingest endpoint (override for staging/local). Deliberately NOT env- overridable.
# Non-HTTPS refused unless the host is localhost.
"endpoint": "https://telemetry.nousresearch.com/v1/telemetry",
},
},
"doctor": {
# Per-probe timeout (seconds) for `hermes doctor --live` real-call probes.
"live_probe_timeout": 10,
},
"updates": {
# Passive version/banner checks only; explicit `hermes update --check` remains enabled.
"check": True,
# Pre-update backup. quick = snapshot small critical state (pairing JSONs, cron jobs,
# config.yaml, .env, auth.json, profile DBs) into <HERMES_HOME>/state-snapshots/, skipping
# files >1 GiB; restore via ``/snapshot``. full = quick PLUS a ``hermes backup`` zip in
# <HERMES_HOME>/backups/ (``hermes import`` restores; slow on large homes; ``--backup``
# forces once). off = none (``--no-backup`` forces once). Legacy booleans: true -> full,
# false -> off.
# Pre-update safety backup — ONE consolidated mechanism, three modes: Files over 1 GiB (e.g. a
# bloated state.db) are skipped with a warning so the snapshot stays fast. This is the #48200
# (wrong-path wipe) safety net.
"pre_update_backup": "quick",
# Full backup zips to retain (older pruned after each success; floored to 1 so the newest is
# always kept). The quick snapshot always keeps exactly 1.
"backup_keep": 5,
# Uncommitted source-tree changes during NON-interactive updates (desktop, gateway — no TTY;
# interactive updates always stash and ask). stash = stash, pull, restore on top (conflicts
# stay in a git stash). discard = stash and drop after the pull (stash-and-drop, not reset
# --hard + clean -fd, so ignored paths like node_modules/venv are never touched).
"non_interactive_local_changes": "stash",
# If the checkout is parked on a feature branch and the tree is clean, switch to the update
# target (commits stay on the branch; a loud notice names it) so non-interactive updates
# keep working. A DIRTY tree blocks the switch and the code update is SKIPPED with a loud
# warning. False = never auto-switch.
"auto_switch_parked_branch": True,
# Clean parked branch with unmerged commits: switch = move to the update target, commits
# stay on the branch (never conflicts). update_in_place = for a maintained custom branch:
# merge origin/<target> INTO it after leaving a pre-update-<stamp> tag; a conflict stops the
# update cleanly. `hermes update --switch-branch` overrides to switch for one run.
"parked_branch_strategy": "switch",
# Refresh an installed cua-driver during `hermes update` (best-effort, macOS only). Turn off
# e.g. on non-admin accounts where /Applications isn't writable.
"refresh_cua_driver": True,
},
# LSP diagnostics (pyright, gopls, rust-analyzer...) in the post-write lint check of
# write_file/patch. Runs only when the cwd or edited file is inside a git worktree; otherwise
# dormant and the in-process syntax check is the only tier.
"lsp": {
"enabled": True, # False disables the whole subsystem: no servers, no event loop, no cost.
# document = wait up to wait_timeout seconds for the current file's diagnostics; full = also
# request workspace-wide diagnostics (slower).
"wait_mode": "document",
"wait_timeout": 5.0,
# Budget for the FIRST request against a workspace whose server is not running yet (spawn +
# initialize + the server's initial program build; tsserver on a large project can need a
# minute). Once the client is up, wait_timeout applies again. 0 = same as wait_timeout.
"warmup_timeout": 0.0,
# After a server fails (spawn error or outer timeout) its (server, workspace root) pair is
# skipped. 0 = for the process lifetime (until `hermes lsp restart`); N = retried after N
# seconds, so one transient stall does not silence a workspace forever.
"broken_retry_seconds": 0.0,
# Workspace roots (glob patterns, ~ expanded; a bare path also matches everything under
# it) where no language server runs at all, e.g. one huge monorepo whose server cannot
# finish in budget, while every other workspace keeps its diagnostics. Must be a list —
# any other shape logs a warning and skips LSP for every workspace until fixed.
"exclude_roots": [],
# Missing server binaries: auto = install via npm/go/pip into <HERMES_HOME>/lsp/bin/ on
# first use; manual = only binaries on PATH; off = alias for manual.
"install_strategy": "auto",
# Node package manager for the npm-recipe servers: npm | pnpm | yarn. Installs still land in
# <HERMES_HOME>/lsp/node_modules; a configured manager that is not installed, or an unknown
# value, skips the install (no silent fallback to npm) so a pnpm/yarn supply-chain policy is
# never bypassed.
"package_manager": "npm",
# Idle seconds before a server is shut down (respawned on demand), so long- running
# processes don't accumulate stale children (hundreds of MB + pipe FDs each) across
# worktrees. 0 = keep servers for process lifetime.
"idle_timeout": 600.0,
# Per-server overrides keyed by registry server_id (pyright, gopls...): disabled: true;
# command: ["path/to/server", "--stdio"] (bypasses auto- install); env: {...};
# initialization_options: {...} (merged into LSP initializationOptions).
# A key that is NOT a built-in id declares a custom server (matched before the built-ins):
# command: ["my-ls", "--stdio"]; extensions: [".ext"]; optional root_markers: [...],
# language_id: "..." (didOpen languageId), description: "...". Manual install only.
"servers": {},
},
# X (Twitter) Search via xAI's x_search Responses tool. Registers when xAI creds exist
# (SuperGrok OAuth or XAI_API_KEY) AND the toolset is enabled in `hermes tools`.
"x_search": {
# xAI model for the Responses call; any Grok model with x_search access works.
"model": "grok-4.5",
# Reasoning effort for models that support it; null keeps the model default.
"reasoning_effort": None,
# Request timeout in seconds (minimum 30); complex queries can take 60-120s.
"timeout_seconds": 180,
# Retries on 5xx / ReadTimeout / ConnectionError (backoff 1.5x attempt s, cap 5s).
"retries": 2,
},
# External secret sources — pull credentials from secret managers at startup instead of storing
# them in ~/.hermes/.env.
# Browser credential vault: which login sources browser_vault_list/fill may draw from. The local
# encrypted vault (`hermes vault add`, Desktop → Settings → Credential Vault) is always on.
# External password managers are unlocked per session with a masked master-password prompt;
# headless sessions (cron, webhook, API) never prompt and see them as locked.
"vault": {
"onepassword": {
# Detected managers are login sources unless the user opts out (vault.<name>.enabled: false).
"enabled": True, # `op` CLI: Login items with a website URL become fillable handles.
"account": "", # account shorthand for `op --account`; empty = default account.
"binary_path": "", # absolute path to op; empty = PATH.
# Env var holding a service-account token (headless auth, no unlock prompt). Unset = prompt.
"service_account_token_env": "OP_SERVICE_ACCOUNT_TOKEN",
},
"bitwarden": {
"enabled": True, # `bw` CLI (Password Manager, not Secrets Manager); run `bw login` once first.
"binary_path": "", # absolute path to bw; empty = PATH.
},
},
"secrets": {
# Optional ordering of enabled sources (e.g. [onepassword, bitwarden]); default registration
# order. Mapped sources (explicit VAR→ref) always beat bulk sources (BSM project dumps);
# first claim on a var wins, later ones warn. "sources": [],
"bitwarden": {
"enabled": False, # When false, BSM is never contacted and bws is never auto-installed.
# Env var holding the machine-account token — the one bootstrap secret; lives in
# ~/.hermes/.env (or the shell), never in config.yaml.
"access_token_env": "BWS_ACCESS_TOKEN",
"project_id": "", # UUID of the BSM project to sync from.
# Seconds to reuse a fresh disk/memory cache entry before contacting Bitwarden again. 0
# disables fresh-cache reuse.
"cache_ttl_seconds": 300,
# Last-good fallback for NETWORK/TIMEOUT outages: AES-GCM cache under ~/.hermes/cache/,
# reused up to max_stale_seconds. Auth failures never fall back.
"encrypted_cache": {"enabled": False, "max_stale_seconds": 0},
# BSM values overwrite existing env vars, so rotating in Bitwarden takes effect without
# clearing the matching .env line.
"override_existing": True,
# Auto-download bws into ~/.hermes/bin/ on first use; False = bws must be on PATH.
"auto_install": True,
# Passed to bws as BWS_SERVER_URL. Empty = US Cloud (bws default);
# https://vault.bitwarden.eu for EU; your own URL for self-hosted.
"server_url": "",
},
"onepassword": {
"enabled": False, # When false, the op CLI is never invoked.
"env": {}, # env-var name → op://vault/item/field; each resolved with one `op read`.
# Account shorthand / sign-in address for `op read --account`; empty = default.
"account": "",
# Env var holding a service-account token for headless auth (exported to op as
# OP_SERVICE_ACCOUNT_TOKEN). Unset = interactive/desktop op session.
"service_account_token_env": "OP_SERVICE_ACCOUNT_TOKEN",
# Absolute path to op, used verbatim (avoids trusting PATH). Empty = PATH.
"binary_path": "",
# Seconds to cache values in-process and on disk; 0 disables BOTH layers.
"cache_ttl_seconds": 300,
# Overwrite existing env vars so rotation takes effect; False lets .env win.
"override_existing": True,
},
},
# Paste collapse thresholds (TUI + CLI); 0 disables each. threshold: bracketed pastes with this
# many newlines collapse to a file reference. fallback: same test for terminals without
# bracketed paste, gated by chars/newlines-added heuristics. char_threshold: single-line pastes
# this long collapse too.
"paste_collapse_threshold": 5,
"paste_collapse_threshold_fallback": 5,
"paste_collapse_char_threshold": 2000,
# Bot Desktop: a headless Xfce screen per profile on the gateway host (Linux), streamed to Hermes
# Desktop where a human can watch, take over (logins, 2FA, CAPTCHAs) and hand back. `hermes computer-use screen`.
"bot_desktop": {
"geometry": "1440x900",
# Opt-in: start the screen automatically the first time computer_use needs a display on a headless
# host. Off by default so installing TigerVNC for other reasons never yields a screen nobody asked
# for; Hermes Desktop's Screen pane offers Start and this toggle.
"auto_start": False,
# Refuse to start below this much free memory (MB), measured on the host or its container cgroup,
# whichever is tighter. Xvnc + Xfce idle at ~220 MB and a takeover's browser adds 0.5-1 GB, so a
# screen with one page runs past 1 GB; the kernel OOM killer picks its victim by score, so on a
# small instance the loser is the dashboard or the gateway rather than the desktop. 0 disables the
# check.
"min_free_memory_mb": 1536,
# Stop a screen nobody has used (no computer_use action, browser spawn, viewer or takeover) for this
# long; it restarts on the next use. Idle Xvnc + Xfce hold ~220 MB, an abandoned browser far more.
# 0 keeps screens up until stopped.
"idle_stop_minutes": 30,
},
"computer_use": {
# cua-driver's upstream PostHog telemetry defaults ON; Hermes sets
# CUA_DRIVER_RS_TELEMETRY_ENABLED=0 in every child env unless this is true.
"cua_telemetry": False,
# Windows only: opt IN to the per-boot cua-driver-serve logon task. False (default)
# keeps the driver on-demand — Computer Use starts it per session, exactly as on
# macOS/Linux, and install/enable flows register no scheduled task (#97389). True
# registers (or repairs) the task at install time — needed to drive Windows over SSH,
# where Session 0 has no interactive desktop (see the computer-use guide).
"autostart": False,
"native_wayland": False,
# Cap driver screenshot longest edge (pixels) via set_config at session start; shrinks SOM
# multimodal payloads. 0 disables.
"max_image_dimension": 1456,
# capture_after mode: som = screenshot + overlays; ax = elements only, no PNG (faster);
# vision = pixels only.
"capture_after_mode": "som",
# Bound cua-driver's accessibility-tree WALK on every capture (get_window_state max_elements).
# _DEFAULT_MAX_ELEMENTS in tools/computer_use/tool.py caps the SURFACED element list at 100 and
# spills the rest to a cache file, so an unbounded walk pays for nodes the model never sees:
# measured on macOS (cua-driver 0.28.2, M-series) a 1,444-node Chrome window went 540 ms -> 83 ms
# and a 456-node Finder window 6.9 s -> 0.6 s at 200, with the returned elements a prefix of the
# unbounded walk. 0 = driver default (2,000 elements / depth 25) — the pre-fix behaviour.
# ~400 keeps the full first 100 visible elements on a pathological tree, at ~1.4 s on Finder;
# the walk's cost grows with the bound, so keep it in the low hundreds. This caps the nodes
# COLLECTED, not the walk's wall clock: a target whose AX surface exceeds the driver's own 20 s
# walk timeout still fails at every bound (measured; a depth bound does not help there either).
"ax_max_elements": 200,
# Disable cua-driver's cursor overlay, which can peg a core when idle (macOS redraw loop;
# Linux/WSL2 idle spin). None = auto (off on macOS + headless/ WSL2 Linux, on elsewhere);
# True = always disable; False = always enable.
# The overlay shows where agent actions land but can peg a core when idle (macOS vImage redraw loop
# #47032; Linux/WSL2 idle spin #28152). cua-driver ≥ 0.6.x supports --no-overlay; Hermes also calls
# set_agent_cursor_enabled(false) after start_session when this is on.
"no_overlay": None,
# standard = cua-driver's own approval boundary; bounded = no runtime prompts, anything
# outside capability_manifest fails closed. `unrestricted` is NOT accepted here: it stays on
# the per-session YOLO toggle so config can't bypass approvals.
"permission_mode": "standard",
# Path (~ ok) to the reviewed manifest for permission_mode bounded; passed as
# --capability-manifest. See cua.ai/docs/reference/cua-driver/permission-modes
"capability_manifest": "",
# macOS only: allow an UNSIGNED CuaDriver.app for the private-session daemon. False fails
# closed unless signed with the official com.trycua.driver identity. Only for local driver
# development from source.
"allow_unsigned_driver": False,
},
# Egress credential-injection proxy (iron-proxy) for remote terminal sandboxes (Docker today):
# the sandbox sees opaque tokens and iron-proxy swaps in real credentials at egress, so a
# compromised sandbox leaks only tokens that work behind the trusted proxy. Configure with
# `hermes egress setup`.
"proxy": {
"enabled": False, # When false, nothing starts, no docker mounts, no binary installs.
# Tunnel listener port; sandboxes get HTTPS_PROXY=http://<host>:<port>.
"tunnel_port": 9090,
# Auto-download the pinned binary into ~/.hermes/bin/; False = iron-proxy on PATH.
"auto_install": True,
# Upstream secret source: env = process env; bitwarden = refetch via `bws secret list` on
# each proxy restart (requires secrets.bitwarden.enabled).
"credential_source": "env",
# True: the Docker backend refuses to start a sandbox if the proxy is enabled but not
# running. False: fall back to direct outbound with real credentials.
"enforce_on_docker": True,
# With credential_source bitwarden, a missing BWS token/project_id or an empty fetch makes
# the daemon raise. True silently falls back to host env (useful mid-migration). A leftover
# fail_on_uncovered_providers key is ignored.
"allow_env_fallback": False,
# SSRF deny list. None/empty = loopback, link-local (incl. 169.254.169.254 metadata),
# RFC1918. Explicit [] opts out (only for hermetic loopback tests).
"upstream_deny_cidrs": None,
# Extra allowed upstream hosts beyond the bundled major-provider defaults; wildcards
# (`*.foo.com`) supported.
"extra_allowed_hosts": [],
},
"desktop": { # Hermes Desktop (Electron) launch options; only affect `hermes desktop`.
# CSS font-family for the app's chat and UI text (e.g. "OpenDyslexic"). Layered in front
# of the active theme's own sans stack so missing glyphs still fall through. Empty = the
# theme's face. The terminal pane is terminal.font_family.
"font_family": "",
# Git repo discovery for the Projects sidebar. Empty roots are a safe
# no-op; users must explicitly configure roots for filesystem scanning.
# Session-derived projects remain available.
"repo_scan_enabled": True,
"repo_scan_roots": [],
"repo_scan_exclude_paths": [],
# Extra Electron flags per launch, e.g. ["--ozone-platform=x11"] or GPU workarounds. List of
# strings; a single string is shell-split.
"electron_flags": [],
# V8 old-space ceiling (MB) for the renderer, applied as --js-flags=--max-old-space-size=N by
# the app itself (also for Start-menu / .desktop launches). 0 = Chromium's default limit.
# A ceiling turns a machine-wide freeze into a bounded renderer reload (#77311).
"renderer_max_old_space_mb": 0,
# Linux Ozone backend, bridged to ELECTRON_OZONE_PLATFORM_HINT (explicit env wins). auto =
# Chromium default; x11 = XWayland, for compositors that ignore always-on-top for Wayland
# clients (e.g. COSMIC) — also puts the HUD on the solid-window input path; wayland = force
# a native Wayland surface.
# See #84011.
"ozone_platform_hint": "auto",
# Bridged to HERMES_DESKTOP_DISABLE_GPU: auto = disable GPU only on remote displays
# (SSH/VNC/RDP); true = always software rendering (no-GPU VMs where the GPU path hangs);
# false = always keep GPU on.
"disable_gpu": "auto",
# Linux keychain for token storage (Chromium --password-store). auto = detect KWallet (KDE
# env) or any org.freedesktop.secrets provider via D-Bus;
# gnome-libsecret|kwallet|kwallet5|kwallet6|basic force one (basic = unencrypted). Bridged
# to HERMES_DESKTOP_PASSWORD_STORE; ignored off-Linux.
"password_store": "auto",
# Expose the renderer's accessibility tree to the OS (macOS/Windows) so dictation/IME tools
# that insert text via the accessibility APIs can reach the composer (#118271, #92607).
# False bridges to HERMES_DESKTOP_RENDERER_ACCESSIBILITY=0 and skips the tree (perf opt-out).
"renderer_accessibility": True,
# Linux: False preserves an existing custom XDG launcher entry; missing entries
# are still created. True keeps the generated entry current on each launch.
"manage_launcher_entry": True,
# macOS only: code-signing identity (login-keychain cert; self-signed works) to re-sign
# locally rebuilt apps so the Designated Requirement — and thus TCC grants — survives
# updates. Empty = default ad-hoc identifier-pinned signing.
"macos_signing_identity": "",
# Auto-continue a turn killed by a crash: resuming re-submits the interrupted prompt if
# fresh; a stale one just shows the recovered partial transcript.
"auto_continue": {
"enabled": True,
# How recent the interruption must be to auto-continue (minutes).
"freshness_minutes": 15,
"max_attempts": 2, # Crash-loop breaker: max automatic re-runs of one interrupted turn.
},
},
"nous": {
# Upper bound (seconds) on the Nous auth keepalive tick, which derives from the
# server-issued credential lifetime (raising above it has no effect). 0 disables the
# keepalive thread.
"keepalive_interval_seconds": 900,
# anthropic_wire: which Portal route carries anthropic/* models. "chat" =
# /v1/chat/completions (default for now); "native" = /v1/messages, the Anthropic
# Messages wire (signed thinking passthrough, native cache_control scopes); "auto" =
# start on chat and, per session, switch to native from the first response when the
# Portal upstream serving the model is one where native is known clean. Native is the
# better wire but on the OpenRouter-served path it re-writes the previous turn's cache on
# 14-20% of consecutive calls in concurrent tool loops (measured 2026-09-06;
# NousResearch/api#227), so chat is the default until that is fixed.
"anthropic_wire": "chat",
# Nous free tier: with no other provider configured, Hermes sets up a free Nous identity on
# first use (inference on nous/welcome + connectors) and offers `/login` (terminal:
# `hermes auth upgrade`) to sign in. false turns the free tier off entirely: nothing is set
# up and nothing is used.
"guest": True,
},
# Google Vertex AI (Gemini). Auth is OAuth2 from a service-account JSON or ADC, NOT an API key;
# the credential path lives in .env (VERTEX_CREDENTIALS_PATH / GOOGLE_APPLICATION_CREDENTIALS).
# Bridged to VERTEX_PROJECT_ID / VERTEX_REGION.
"vertex": {
# GCP project ID. Empty → project_id from the service-account JSON (or ADC).
"project_id": "",
# "global" is required for Gemini 3.x preview models (regional endpoints silently 404); use
# e.g. "us-central1" only if your models are region-pinned.
"region": "global",
},
# Managed llama.cpp runtime (docs: user-guide/local-models): official binaries, one supervised
# llama-server in router mode. No context/VRAM knobs by design.
"local_runtime": {
# Off = detection-only (Hermes still finds an external llama-server you run).
"enabled": False,
# Engine versions and every dependent library are pinned by pm/lock.json.
# auto = CUDA on NVIDIA, Metal on macOS, Vulkan on other GPUs, else CPU. Explicit:
# cuda|metal|vulkan|hip|cpu.
"backend": "auto",
"models_max": 4, # Router process: how many models may be resident at once.
"port": 0, # Port for the managed server. 0 = pick a free port at spawn.
# Extra ports detection probes for an external llama-server (besides 8080).
"detect_ports": [],
},
"_config_version": 46, # Config schema version - bump this when adding new required fields
}
def _env(description, prompt, **keys):
"""One OPTIONAL_ENV_VARS entry; keyword order is preserved as dict key order."""
return {"description": description, "prompt": prompt, **keys}
_OMIT = object()
def _category(category, password, advanced):
"""Entry factory for one category with its usual password/advanced defaults.
``url``/``help``/``tools`` are only written when passed; ``password=None`` omits the key;
``advanced`` is only written when true. Key order matches the plain ``_env`` entries.
"""
def make(description, prompt, url=_OMIT, *, help=_OMIT, tools=_OMIT, password=password,
advanced=advanced):
d = {"description": description, "prompt": prompt}
d.update((k, v) for k, v in (("help", help), ("url", url), ("tools", tools)) if v is not _OMIT)
if password is not None:
d["password"] = password
d["category"] = category
if advanced:
d["advanced"] = True
return d
return make
_prov = _category("provider", password=True, advanced=True)
_tool = _category("tool", password=True, advanced=False)
_msg = _category("messaging", password=False, advanced=False)
_skill = _category("skill", password=True, advanced=True)
_setting = _category("setting", password=False, advanced=False)
def _base_url(name, prompt_name=None):
"""Provider ``*_BASE_URL`` override entry (advanced, not a secret)."""
prompt = f"{prompt_name or name} base URL (leave empty for default)"
return _prov(f"{name} base URL override", prompt, None, password=False)
# Optional environment variables that enhance functionality. Feeds the dashboard keys page and setup
# checklists; category: provider|tool|skill|messaging|setting, advanced=True hides from checklists,
# tools=[...] lists the model tools the key unlocks.
OPTIONAL_ENV_VARS = {
# ── Provider (handled in provider selection, not shown in checklists) ──
"NOUS_BASE_URL": _base_url("Nous Portal"),
"HERMES_ANON_API_SECRET": _env(
"Shared secret for the Nous free-tier sign-up endpoints while they are in their gated "
"integration phase (not needed once the gate is removed)",
"Nous free-tier shared secret (leave empty unless given one)", password=True,
category="provider", advanced=True),
"OPENROUTER_API_KEY": _env("OpenRouter API key (for vision, web scraping helpers, and MoA)",
"OpenRouter API key", url="https://openrouter.ai/keys", password=True, tools=["vision_analyze"],
category="provider", advanced=True),
"GOOGLE_API_KEY": _prov("Google AI Studio API key (also recognized as GEMINI_API_KEY)",
"Google AI Studio API key", "https://aistudio.google.com/app/apikey"),
"GEMINI_API_KEY": _prov("Google AI Studio API key (alias for GOOGLE_API_KEY)", "Gemini API key",
"https://aistudio.google.com/app/apikey"),
"GEMINI_BASE_URL": _base_url("Google AI Studio", "Gemini"),
"VERTEX_CREDENTIALS_PATH": _prov(
"Path to a Google Cloud service account JSON for Vertex AI (Gemini). Vertex uses "
"OAuth2, not a static API key — this points at the credentials Hermes mints short-lived "
"tokens from. Falls back to GOOGLE_APPLICATION_CREDENTIALS, then to ADC (gcloud auth "
"application-default login). Set project/region under vertex: in config.yaml.",
"Vertex service account JSON path (leave empty to use ADC / "
"GOOGLE_APPLICATION_CREDENTIALS)", "https://cloud.google.com/iam/docs/keys-create-delete",
password=False),
"XAI_API_KEY": _prov("xAI API key", "xAI API key", "https://console.x.ai/"),
"XAI_BASE_URL": _base_url("xAI"),
"NVIDIA_API_KEY": _prov("NVIDIA NIM API key (build.nvidia.com or local NIM endpoint)",
"NVIDIA NIM API key", "https://build.nvidia.com/"),
"NVIDIA_BASE_URL": _prov(
"NVIDIA NIM base URL override (e.g. http://localhost:8000/v1 for local NIM)",
"NVIDIA NIM base URL (leave empty for default)", None, password=False),
"LM_API_KEY": _prov("LM Studio bearer token for auth-enabled local servers",
"LM Studio API key / bearer token", None),
"LM_BASE_URL": _base_url("LM Studio"),
"GLM_API_KEY": _prov("Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)",
"Z.AI / GLM API key", "https://z.ai/"),
"ZAI_API_KEY": _prov("Z.AI API key (alias for GLM_API_KEY)", "Z.AI API key", "https://z.ai/"),
"Z_AI_API_KEY": _prov("Z.AI API key (alias for GLM_API_KEY)", "Z.AI API key", "https://z.ai/"),
"GLM_BASE_URL": _base_url("Z.AI / GLM"),
"KIMI_API_KEY": _prov("Kimi / Moonshot API key", "Kimi API key",
"https://platform.moonshot.cn/"),
"KIMI_BASE_URL": _base_url("Kimi / Moonshot", "Kimi"),
"KIMI_CN_API_KEY": _prov("Kimi / Moonshot China API key", "Kimi (China) API key",
"https://platform.moonshot.cn/"),
"STEPFUN_API_KEY": _prov("StepFun Step Plan API key", "StepFun Step Plan API key",
"https://platform.stepfun.com/"),
"STEPFUN_BASE_URL": _base_url("StepFun Step Plan"),
"ARCEEAI_API_KEY": _prov("Arcee AI API key", "Arcee AI API key", "https://chat.arcee.ai/"),
"ARCEE_BASE_URL": _base_url("Arcee AI", "Arcee"),
"GMI_API_KEY": _prov("GMI Cloud API key", "GMI Cloud API key", "https://www.gmicloud.ai/"),
"GMI_BASE_URL": _base_url("GMI Cloud"),
"ACTUAL_API_KEY": _prov("Actual Computer inference key (ac_...)",
"Actual Computer inference key", "https://actual.inc/user/keys"),
"FIREWORKS_API_KEY": _prov("Fireworks AI API key", "Fireworks AI API key",
"https://app.fireworks.ai/settings/users/api-keys"),
"MINIMAX_API_KEY": _prov("MiniMax API key (international)", "MiniMax API key",
"https://www.minimax.io/"),
"MINIMAX_BASE_URL": _base_url("MiniMax"),
"MINIMAX_CN_API_KEY": _prov("MiniMax API key (China endpoint)", "MiniMax (China) API key",
"https://www.minimaxi.com/"),
"MINIMAX_CN_BASE_URL": _base_url("MiniMax (China)"),
"DEEPSEEK_API_KEY": _prov("DeepSeek API key for direct DeepSeek access", "DeepSeek API Key",
"https://platform.deepseek.com/api_keys", advanced=False),
"DEEPSEEK_BASE_URL": _prov("Custom DeepSeek API base URL (advanced)", "DeepSeek Base URL", "",
password=False, advanced=False),
"DASHSCOPE_API_KEY": _prov("Alibaba Cloud DashScope API key (Qwen + multi-provider models)",
"DashScope API Key", "https://modelstudio.console.alibabacloud.com/", advanced=False),
"DASHSCOPE_BASE_URL": _prov(
"Custom DashScope base URL (default: coding-intl OpenAI-compat endpoint)",
"DashScope Base URL", "", password=False),
"HERMES_QWEN_BASE_URL": _prov(
"Qwen Portal base URL override (default: https://portal.qwen.ai/v1)",
"Qwen Portal base URL (leave empty for default)", None, password=False),
"OPENCODE_ZEN_API_KEY": _prov("OpenCode Zen API key (pay-as-you-go access to curated models)",
"OpenCode Zen API key", "https://opencode.ai/auth"),
"COMMANDCODE_API_KEY": _prov(
"CommandCode API key (GOAT/Pro/Max/Provider plans — 30+ models via one key)",
"CommandCode API key", "https://commandcode.ai/studio/"),
"OPENCODE_ZEN_BASE_URL": _base_url("OpenCode Zen"),
"OPENCODE_GO_API_KEY": _prov("OpenCode Go API key ($10/month subscription for open models)",
"OpenCode Go API key", "https://opencode.ai/auth"),
"OPENCODE_GO_BASE_URL": _base_url("OpenCode Go"),
"HF_TOKEN": _prov(
"Hugging Face token for Inference Providers (20+ open models via router.huggingface.co)",
"Hugging Face Token", "https://huggingface.co/settings/tokens", advanced=False),
"HF_BASE_URL": _base_url("Hugging Face Inference Providers", "HF"),
"OLLAMA_API_KEY": _prov("Ollama Cloud API key (ollama.com — cloud-hosted open models)",
"Ollama Cloud API key", "https://ollama.com/settings"),
"OLLAMA_BASE_URL": _prov("Ollama Cloud base URL override (default: https://ollama.com/v1)",
"Ollama base URL (leave empty for default)", None, password=False),
"XIAOMI_API_KEY": _prov(
"Xiaomi MiMo API key for MiMo models (mimo-v2.5-pro, mimo-v2.5, mimo-v2-pro, "
"mimo-v2-omni, mimo-v2-flash)", "Xiaomi MiMo API Key", "https://platform.xiaomimimo.com",
advanced=False),
"XIAOMI_BASE_URL": _prov(
"Xiaomi MiMo base URL override (default: https://api.xiaomimimo.com/v1)",
"Xiaomi base URL (leave empty for default)", None, password=False),
"UPSTAGE_API_KEY": _prov("Upstage API key for Solar LLM models", "Upstage API Key",
"https://console.upstage.ai/api-keys", advanced=False),
"UPSTAGE_BASE_URL": _prov("Upstage base URL override (default: https://api.upstage.ai/v1)",
"Upstage base URL (leave empty for default)", None, password=False),
"AWS_REGION": _prov("AWS region for Bedrock API calls (e.g. us-east-1, eu-central-1)",
"AWS Region", "https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html",
password=False),
"AWS_PROFILE": _prov("AWS named profile for Bedrock authentication (from ~/.aws/credentials)",
"AWS Profile", None, password=False),
"AZURE_FOUNDRY_API_KEY": _prov("Azure Foundry API key for custom Azure endpoints",
"Azure Foundry API Key", "https://ai.azure.com/", advanced=False),
"AZURE_FOUNDRY_BASE_URL": _prov(
"Azure Foundry base URL (set via 'hermes model' for endpoint-specific config)",
"Azure Foundry base URL", None, password=False),
# ── Tool API keys ──
"EXA_API_KEY": _tool("Exa API key for AI-native web search and contents", "Exa API key",
"https://exa.ai/", tools=["web_search", "web_extract"]),
"PARALLEL_API_KEY": _tool("Parallel API key for AI-native web search and extract",
"Parallel API key", "https://parallel.ai/", tools=["web_search", "web_extract"]),
"FIRECRAWL_API_KEY": _tool("Firecrawl API key for web search and scraping", "Firecrawl API key",
"https://firecrawl.dev/", tools=["web_search", "web_extract"]),
"FIRECRAWL_API_URL": _tool("Firecrawl API URL for self-hosted instances (optional)",
"Firecrawl API URL (leave empty for cloud)", None, password=False, advanced=True),
"FIRECRAWL_GATEWAY_URL": _tool(
"Exact Firecrawl tool-gateway origin override for Nous Subscribers only (optional)",
"Firecrawl gateway URL (leave empty to derive from domain)", None, password=False,
advanced=True),
"TOOL_GATEWAY_URL": _tool(
"Exact shared tool-gateway origin for on-origin vendors and media uploads (optional)",
"Shared tool-gateway URL (leave empty to derive from domain)", None,
password=False, advanced=True),
"CONNECTOR_GATEWAY_URL": _tool(
"Exact connector-gateway origin for the connectors API (optional)",
"Connector-gateway URL (leave empty to derive from domain)", None,
password=False, advanced=True),
"TOOL_GATEWAY_DOMAIN": _tool(
"Shared tool-gateway domain suffix for Nous Subscribers only, used to derive vendor "
"hosts, e.g. nousresearch.com -> firecrawl-gateway.nousresearch.com",
"Tool-gateway domain suffix", None, password=False, advanced=True),
"TOOL_GATEWAY_SCHEME": _tool(
"Shared tool-gateway URL scheme for Nous Subscribers only, used to derive vendor hosts "
"(`https` by default, set `http` for local gateway testing)", "Tool-gateway URL scheme",
None, password=False, advanced=True),
"TOOL_GATEWAY_USER_TOKEN": _tool(
"Explicit Nous Subscriber access token for tool-gateway requests (optional; otherwise "
"read from the Hermes auth store)", "Tool-gateway user token", None, advanced=True),
"TAVILY_API_KEY": _tool(
"Tavily API key for AI-native web search and extract (optional — keyless works when "
"Tavily is selected)", "Tavily API key", "https://app.tavily.com/home",
tools=["web_search", "web_extract"]),
"PERPLEXITY_API_KEY": _tool(
"Perplexity API key for the Search API web backend (ranked results + query-relevant page "
"snippets)", "Perplexity API key", "https://www.perplexity.ai/account/api",
tools=["web_search", "web_extract"]),
"KEENABLE_API_KEY": _tool(
"Keenable API key for fast independent-index web search and page fetch (optional — "
"keyless free tier works without it)", "Keenable API key", "https://keenable.ai",
tools=["web_search", "web_extract"]),
"SEARXNG_URL": _tool("URL of your SearXNG instance for free self-hosted web search",
"SearXNG URL (e.g. http://localhost:8080)", "https://searxng.github.io/searxng/",
tools=["web_search"], password=False),
"BRAVE_SEARCH_API_KEY": _tool(
"Brave Search API subscription token (free tier: 2,000 queries/mo)",
"Brave Search subscription token", "https://brave.com/search/api/", tools=["web_search"]),
"BROWSERBASE_API_KEY": _tool(
"Browserbase API key for cloud browser (optional — local browser works without this)",
"Browserbase API key", "https://browserbase.com/",
tools=["browser_navigate", "browser_click"]),
"BROWSERBASE_PROJECT_ID": _tool(
"Browserbase project ID (optional — only needed for cloud browser)",
"Browserbase project ID", "https://browserbase.com/",
tools=["browser_navigate", "browser_click"], password=False),
"BROWSER_USE_API_KEY": _tool(
"Browser Use API key for cloud browser (optional — local browser works without this)",
"Browser Use API key", "https://browser-use.com/",
tools=["browser_navigate", "browser_click"]),
"FIRECRAWL_BROWSER_TTL": _tool(
"Firecrawl browser session TTL in seconds (optional, default 300)",
"Browser session TTL (seconds)", tools=["browser_navigate", "browser_click"],
password=False),
"AGENT_BROWSER_ENGINE": _env(
"Local browser engine: auto (default Chrome), lightpanda (faster, no screenshots; Browser Use mode "
"spawns lightpanda serve), chrome", "Browser engine (auto/lightpanda/chrome)",
url="https://lightpanda.io/docs/run-locally/installation/one-liner",
tools=["browser_exec", "browser_navigate", "browser_snapshot", "browser_click", "browser_vision"],
password=False, category="tool", advanced=True),
"CAMOFOX_URL": _tool(
"Camofox browser server URL for local anti-detection browsing (e.g. http://localhost:9377)",
"Camofox server URL", "https://github.com/jo-inc/camofox-browser",
tools=["browser_navigate", "browser_click"], password=False),
"CAMOFOX_API_KEY": _tool(
"Optional bearer token sent as Authorization header to a remote/authenticated Camofox "
"server", "Camofox API key", "https://github.com/jo-inc/camofox-browser",
tools=["browser_navigate", "browser_click"], advanced=True),
"FAL_KEY": _tool("FAL API key for image and video generation", "FAL API key", "https://fal.ai/",
tools=["image_generate", "video_generate"]),
"KREA_API_KEY": _tool("Krea API key for Krea 2 image generation (Medium + Large)",
"Krea API key", "https://www.krea.ai/settings/api-tokens", tools=["image_generate"]),
"VOICE_TOOLS_OPENAI_KEY": _tool(
"OpenAI API key for voice transcription (Whisper) and OpenAI TTS",
"OpenAI API Key (for Whisper STT + TTS)", "https://platform.openai.com/api-keys",
tools=["voice_transcription", "openai_tts"]),
"ELEVENLABS_API_KEY": _tool(
"ElevenLabs API key for premium text-to-speech voices and Scribe transcription",
"ElevenLabs API key", "https://elevenlabs.io/",
tools=["elevenlabs_tts", "voice_transcription"]),
"MISTRAL_API_KEY": _tool("Mistral API key for Voxtral TTS and transcription (STT)",
"Mistral API key", "https://console.mistral.ai/"),
"PORCUPINE_ACCESS_KEY": _tool(
"Picovoice access key for the Porcupine 'Hey Hermes' wake word engine (optional; "
"openWakeWord is the free default)", "Picovoice access key",
"https://console.picovoice.ai/"),
"GITHUB_TOKEN": _tool("GitHub token for Skills Hub (higher API rate limits, skill publish)",
"GitHub Token", "https://github.com/settings/tokens"),
# ── Bundled skills (opt-in) ── category="skill" (not "tool") so the sandbox env blocklist in
# tools/environments/local.py does NOT rewrite them; skills need them passed through to curl
# via tools/env_passthrough.py.
"NOTION_API_KEY": _skill("Notion integration token (used by the `notion` skill)",
"Notion API key", "https://www.notion.so/my-integrations"),
"LINEAR_API_KEY": _skill("Linear personal API key (used by the `linear` skill)",
"Linear API key", "https://linear.app/settings/account/security"),
"AIRTABLE_API_KEY": _skill("Airtable personal access token (used by the `airtable` skill)",
"Airtable API key", "https://airtable.com/create/tokens"),
"TENOR_API_KEY": _skill("Tenor API key for GIF search (used by the `gif-search` skill)",
"Tenor API key", "https://developers.google.com/tenor/guides/quickstart"),
# ── Honcho ──
"HONCHO_API_KEY": _tool("Honcho API key for AI-native persistent memory", "Honcho API key",
"https://app.honcho.dev", tools=["honcho_context"]),
"HONCHO_BASE_URL": _tool("Base URL for self-hosted Honcho instances (no API key needed)",
"Honcho base URL (e.g. http://localhost:8000)", password=None),
# ── Hindsight ──
"HINDSIGHT_API_KEY": _tool("Hindsight API key for graph-aware persistent memory",
"Hindsight API key", "https://hindsight.vectorize.io", tools=["hindsight_recall"]),
"HINDSIGHT_API_URL": _tool(
"Base URL for the Hindsight API (default: https://api.hindsight.vectorize.io)",
"Hindsight API URL", password=None, advanced=True),
# ── Supermemory ──
"SUPERMEMORY_API_KEY": _tool("Supermemory API key for conversation-scoped persistent memory",
"Supermemory API key", "https://supermemory.ai", tools=["supermemory_search"]),
# ── Mem0 ──
"MEM0_API_KEY": _tool("Mem0 Platform API key for semantic persistent memory", "Mem0 API key",
"https://app.mem0.ai", tools=["mem0_search"]),
# ── RetainDB ──
"RETAINDB_API_KEY": _tool("RetainDB API key for persistent memory", "RetainDB API key",
"https://retaindb.com", tools=["retaindb_search"]),
"RETAINDB_BASE_URL": _tool(
"Base URL for self-hosted RetainDB instances (default: https://api.retaindb.com)",
"RetainDB base URL", password=None, advanced=True),
# ── ByteRover ──
"BRV_API_KEY": _tool("ByteRover API key (optional, for cloud sync — local-first by default)",
"ByteRover API key", "https://app.byterover.dev", tools=["brv_query"]),
# ── OpenViking ──
"OPENVIKING_API_KEY": _tool("OpenViking API key (leave blank for local dev mode)",
"OpenViking API key", tools=["viking_search"]),
"OPENVIKING_ENDPOINT": _tool("OpenViking server URL (default: http://127.0.0.1:1933)",
"OpenViking endpoint", password=None, advanced=True),
# ── Langfuse observability ──
"HERMES_LANGFUSE_PUBLIC_KEY": _tool("Langfuse project public key (pk-lf-...)",
"Langfuse public key", "https://cloud.langfuse.com", password=False),
"HERMES_LANGFUSE_SECRET_KEY": _tool("Langfuse project secret key (sk-lf-...)",
"Langfuse secret key", "https://cloud.langfuse.com"),
"HERMES_LANGFUSE_BASE_URL": _tool("Langfuse server URL (default: https://cloud.langfuse.com)",
"Langfuse server URL (leave empty for cloud.langfuse.com)", None, password=False,
advanced=True),
# ── Messaging platforms ──
"TELEGRAM_BOT_TOKEN": _msg(
"Complete Telegram bot token created by @BotFather (numeric bot ID followed by a colon "
"and secret)", "Telegram bot token", "https://t.me/BotFather", password=True),
"TELEGRAM_ALLOWED_USERS": _msg(
"Optional comma-separated numeric Telegram user IDs allowed immediately; leave blank to "
"approve new users through DM pairing", "Allowed Telegram user IDs (comma-separated)",
"https://t.me/userinfobot"),
"TELEGRAM_PROXY": _msg(
"Proxy URL for Telegram connections (overrides HTTPS_PROXY). Supports http://, "
"https://, socks5://", "Telegram proxy URL (optional)"),
"DISCORD_BOT_TOKEN": _msg("Discord bot token from Developer Portal", "Discord bot token",
"https://discord.com/developers/applications", password=True),
"DISCORD_ALLOWED_USERS": _msg("Comma-separated Discord user IDs allowed to use the bot",
"Allowed Discord user IDs (comma-separated)", None),
"DISCORD_REPLY_TO_MODE": _msg(
"Discord reply threading mode: 'off' (no reply references), 'first' (reply on first "
"message only, default), 'all' (reply on every chunk)",
"Discord reply mode (off/first/all)", None),
"SLACK_BOT_TOKEN": _msg(
"Slack bot token (xoxb-). Get from OAuth & Permissions after installing your app. "
"Required scopes: chat:write, app_mentions:read, channels:history, groups:history, "
"im:history, im:read, im:write, mpim:history, mpim:read, users:read, files:read, "
"files:write", "Slack Bot Token (xoxb-...)", "https://api.slack.com/apps",
help=("In your Slack app, add the required bot scopes, install the app to the workspace, "
"then copy OAuth & Permissions > Bot User OAuth Token."), password=True),
"SLACK_APP_TOKEN": _msg(
"Slack app-level token (xapp-) for Socket Mode. Get from Basic Information → App-Level "
"Tokens. Also ensure Event Subscriptions include: message.im, message.channels, "
"message.groups, message.mpim, app_mention", "Slack App Token (xapp-...)",
"https://api.slack.com/apps",
help=("In your Slack app, enable Socket Mode, then create Basic Information > App-Level "
"Tokens with the connections:write scope."), password=True),
"SLACK_ALLOWED_USERS": _msg(
"Comma-separated Slack member IDs allowed to use Hermes, e.g. U01ABC2DEF3. Without "
"this, Slack may connect but deny messages by default.", "Allowed Slack member IDs",
"https://api.slack.com/apps",
help=("In Slack, open your profile, choose More or the three-dot menu, then Copy member "
"ID. Add multiple IDs comma-separated.")),
"MATTERMOST_URL": _msg("Mattermost server URL (e.g. https://mm.example.com)",
"Mattermost server URL", "https://mattermost.com/deploy/"),
"MATTERMOST_TOKEN": _msg("Mattermost bot token or personal access token",
"Mattermost bot token", None, password=True),
"MATTERMOST_ALLOWED_USERS": _msg("Comma-separated Mattermost user IDs allowed to use the bot",
"Allowed Mattermost user IDs (comma-separated)", None),
"MATTERMOST_REQUIRE_MENTION": _msg(
"Require @mention in Mattermost channels (default: true). Set to false to respond to "
"all messages.", "Require @mention in channels", None),
"MATTERMOST_FREE_RESPONSE_CHANNELS": _msg(
"Comma-separated Mattermost channel IDs where bot responds without @mention",
"Free-response channel IDs (comma-separated)", None),
"MATRIX_HOMESERVER": _msg("Matrix homeserver URL (e.g. https://matrix.example.org)",
"Matrix homeserver URL", "https://matrix.org/ecosystem/servers/"),
"MATRIX_ACCESS_TOKEN": _msg("Matrix access token (preferred over password login)",
"Matrix access token", None, password=True),
"MATRIX_USER_ID": _msg("Matrix user ID (e.g. @hermes:example.org)",
"Matrix user ID (@user:server)", None),
"MATRIX_ALLOWED_USERS": _msg(
"Comma-separated Matrix user IDs allowed to use the bot (@user:server format)",
"Allowed Matrix user IDs (comma-separated)", None),
"MATRIX_REQUIRE_MENTION": _msg(
"Require @mention in Matrix rooms (default: true). Set to false to respond to all "
"messages.", "Require @mention in rooms (true/false)", None, advanced=True),
"MATRIX_FREE_RESPONSE_ROOMS": _msg(
"Comma-separated Matrix room IDs where bot responds without @mention",
"Free-response room IDs (comma-separated)", None, advanced=True),
"MATRIX_AUTO_THREAD": _msg("Auto-create threads for messages in Matrix rooms (default: true)",
"Auto-create threads in rooms (true/false)", None, advanced=True),
"MATRIX_DM_AUTO_THREAD": _msg("Auto-create threads for DM messages in Matrix (default: false)",
"Auto-create threads in DMs (true/false)", None, advanced=True),
"MATRIX_DEVICE_ID": _msg(
"Stable Matrix device ID for E2EE persistence across restarts (e.g. HERMES_BOT)",
"Matrix device ID (stable across restarts)", None, advanced=True),
"MATRIX_RECOVERY_KEY": _msg(
"Matrix recovery key for cross-signing verification after device key rotation (from "
"Element: Settings → Security → Recovery Key)", "Matrix recovery key", None, password=True,
advanced=True),
"BLUEBUBBLES_SERVER_URL": _msg(
"BlueBubbles server URL for iMessage integration (e.g. http://192.168.1.10:1234)",
"BlueBubbles server URL", "https://bluebubbles.app/"),
"BLUEBUBBLES_PASSWORD": _msg(
"BlueBubbles server password (from BlueBubbles Server → Settings → API)",
"BlueBubbles server password", None, password=True),
"BLUEBUBBLES_ALLOWED_USERS": _msg(
"Comma-separated iMessage addresses (email or phone) allowed to use the bot",
"Allowed iMessage addresses (comma-separated)", None),
"BLUEBUBBLES_ALLOW_ALL_USERS": _msg("Allow all BlueBubbles users without allowlist",
"Allow All BlueBubbles Users", password=None),
"QQ_APP_ID": _msg("QQ Bot App ID from QQ Open Platform (q.qq.com)", "QQ App ID",
"https://q.qq.com", password=None),
"QQ_CLIENT_SECRET": _msg("QQ Bot Client Secret from QQ Open Platform", "QQ Client Secret",
password=True),
"QQ_ALLOWED_USERS": _msg("Comma-separated QQ user IDs allowed to use the bot",
"QQ Allowed Users", password=None),
"QQ_GROUP_ALLOWED_USERS": _msg("Comma-separated QQ group IDs allowed to interact with the bot",
"QQ Group Allowed Users", password=None),
"QQ_ALLOW_ALL_USERS": _msg("Allow all QQ users without an allowlist (true/false)",
"Allow All QQ Users", password=None),
"QQBOT_HOME_CHANNEL": _msg("Default QQ channel/group for cron delivery and notifications",
"QQ Home Channel", password=None),
"QQBOT_HOME_CHANNEL_NAME": _msg("Display name for the QQ home channel", "QQ Home Channel Name",
password=None),
"QQ_SANDBOX": _msg("Enable QQ sandbox mode for development testing (true/false)",
"QQ Sandbox Mode", password=None),
"IRC_SERVER": _msg("IRC server hostname (e.g. irc.libera.chat)", "IRC server", None),
"IRC_CHANNEL": _msg("IRC channel to join (e.g. #hermes)", "IRC channel", None),
"IRC_NICKNAME": _msg("Bot nickname on IRC (default: hermes-bot)", "IRC nickname", None),
"IRC_SERVER_PASSWORD": _msg("IRC server password (if required)", "IRC server password", None,
password=True, advanced=True),
"IRC_NICKSERV_PASSWORD": _msg("NickServ password for nick identification", "NickServ password",
None, password=True, advanced=True),
"GATEWAY_ALLOW_ALL_USERS": _msg(
"Allow all users to interact with messaging bots (true/false). Default: false.",
"Allow all users (true/false)", None, advanced=True),
"API_SERVER_ENABLED": _msg(
"Enable the OpenAI-compatible API server (true/false). Allows frontends like Open "
"WebUI, LobeChat, etc. to connect.", "Enable API server (true/false)", None, advanced=True),
"API_SERVER_KEY": _msg(
"Bearer token for API server authentication. Required whenever the API server is "
"enabled; server refuses to start without it.", "API server auth key", None, password=True,
advanced=True),
"API_SERVER_PORT": _msg("Port for the API server (default: 8642).", "API server port", None,
advanced=True),
"API_SERVER_HOST": _msg(
"Host/bind address for the API server (default: 127.0.0.1). API_SERVER_KEY is still "
"required even on loopback binds.", "API server host", None, advanced=True),
"API_SERVER_MODEL_NAME": _msg(
"Model name advertised on /v1/models. Defaults to the profile name (or 'hermes-agent' "
"for the default profile). Useful for multi-user setups with OpenWebUI.",
"API server model name", None, advanced=True),
"GATEWAY_PROXY_URL": _msg(
"URL of a remote Hermes API server to forward messages to (proxy mode). When set, the "
"gateway handles platform I/O only — all agent work is delegated to the remote server. "
"Use for Docker E2EE containers that relay to a host agent. Also configurable via "
"gateway.proxy_url in config.yaml.",
"Remote Hermes API server URL (e.g. http://192.168.1.100:8642)", None, advanced=True),
"GATEWAY_PROXY_KEY": _msg(
"Bearer token for authenticating with the remote Hermes API server (proxy mode). Must "
"match the API_SERVER_KEY on the remote host.", "Remote API server auth key", None,
password=True, advanced=True),
"WEBHOOK_ENABLED": _msg(
"Enable the webhook platform adapter for receiving events from GitHub, GitLab, etc.",
"Enable webhooks (true/false)", None),
"WEBHOOK_PORT": _msg("Port for the webhook HTTP server (default: 8644).", "Webhook port", None),
"WEBHOOK_SECRET": _msg(
"Global HMAC secret for webhook signature validation (overridable per route in "
"config.yaml).", "Webhook secret", None, password=True),
# ── Agent settings ── (MESSAGING_CWD is gone: use terminal.cwd in config.yaml, which the
# gateway bridges to TERMINAL_CWD.)
"SUDO_PASSWORD": _setting(
"Sudo password for terminal commands requiring root access; set to an explicit empty "
"string to try empty without prompting", "Sudo password", None, password=True),
# HERMES_TOOL_PROGRESS_MODE (deprecated; use display.tool_progress) is intentionally NOT listed:
# this dict feeds user-facing surfaces (dashboard keys page, setup checklists), so deprecated
# knobs stay in config._EXTRA_ENV_KEYS only. HERMES_TOOL_PROGRESS is unsupported.
"HERMES_PREFILL_MESSAGES_FILE": _setting(
"Path to JSON file with ephemeral prefill messages for few-shot priming",
"Prefill messages file path", None),
"HERMES_EPHEMERAL_SYSTEM_PROMPT": _setting(
"Ephemeral system prompt injected at API-call time (never persisted to sessions)",
"Ephemeral system prompt", None),
}