`python3 website/scripts/check_doc_links.py --fix` over the current tree: 126 route-style links in 9 pages (the six that conflicted with #114784/#114806/ #114851 plus google-gemini, cron and secrets) rewritten to relative file paths. Check mode is clean afterwards.
4.9 KiB
Secrets
Hermes can pull API keys from external secret managers at process startup instead of storing them in ~/.hermes/.env. The bootstrap token for the secret manager lives in .env; every other provider key (OpenAI, Anthropic, OpenRouter, etc.) can stay in the manager and rotate centrally.
Supported:
- Bitwarden Secrets Manager —
bwsCLI, lazy-installed, free tier works. - 1Password —
op://references via the officialopCLI; service-account or desktop session auth. - Command helper — any CLI vault (
keepassxc-cli,secret-tool,pass, custom scripts) via a user-configured helper that printsKEY=VALUElines.
Multiple sources at once
You can enable more than one secret source at the same time — for example a team Bitwarden project alongside a personal vault plugin. Sources compose per env var with a deterministic precedence ladder:
- Your
.env/ shell wins by default. A source only replaces a pre-existing value when its ownoverride_existing: trueis set (Bitwarden defaults to true so central rotation works). - Mapped sources beat bulk sources. A source where you explicitly bind env vars to references (an
env:map) outranks a source that injects a whole project of secrets implicitly, regardless of ordering. - First source wins. Within the same shape, the order of the optional
secrets.sourceslist (or registration order) decides. Later claims on an already-claimed var are skipped — with a startup warning, never silently.
override_existing never lets one source overwrite a var another source already claimed, and no source can ever overwrite another source's bootstrap token (e.g. BWS_ACCESS_TOKEN).
secrets:
sources: [bitwarden] # optional explicit ordering
bitwarden:
enabled: true
project_id: "..."
Every credential injected by a source is labelled with its origin — setup flows and hermes model show (from Bitwarden) next to detected keys so you always know where a value came from.
Profiles and shared vaults
Two orchestrator-level knobs make one shared vault safe across profiles:
-
secrets.preserve_existing— a list of env var names whose existing.env/ shell value always wins, even against a source withoverride_existing: true. Use it for per-profile platform secrets (e.g.FEISHU_APP_SECRET) that intentionally differ across profiles while everything else rotates centrally:secrets: preserve_existing: [FEISHU_APP_SECRET, TELEGRAM_BOT_TOKEN] -
Profile aliasing (on by default,
secrets.profile_alias: falseto disable) — when Hermes runs under a named profile, a vault secret namedFOO_<PROFILE>(credential-shaped suffixes only:*_API_KEY,*_TOKEN,*_SECRET,*_KEY,*_PASSWORD) also hydrates the canonicalFOO. StoreTELEGRAM_BOT_TOKEN_MILLAin the shared project and themillaprofile's adapters — which read the fixed nameTELEGRAM_BOT_TOKEN— get the right value automatically. A var the vault supplies directly under its canonical name always beats an alias.
Both apply to every source — bundled and plugin — because they live in the orchestrator, not the backends.
Secrets in child processes
Terminal commands, execute_code sandboxes and no_agent cron scripts run with a sanitized environment: Hermes-managed credentials are stripped, and only variables you declare in terminal.env_passthrough (or a loaded skill's required_environment_variables) are forwarded. A declared variable is forwarded with the owning profile's value — from that profile's .env or its secret sources — even when the profile is served by a multi-profile gateway or the Desktop/dashboard backend and its secrets never entered the process environment. A profile's declared value never reaches another profile's children, and the launch profile's .env credentials are dropped from children that run for a served profile. Provider credentials cannot be declared; see Security → Credential scoping.
Adding your own backend
Third-party secret managers ship as standalone plugins, not core PRs. A backend subclasses agent.secret_sources.base.SecretSource (one required method: fetch(cfg, home_path) -> FetchResult) and registers via ctx.register_secret_source(MySource()) in the plugin's register(ctx). The orchestrator owns precedence, conflict handling, timeouts, and provenance — your source only fetches. Full guide with the contract rules, subprocess-safety helper, and conformance kit: Building a Secret Source Plugin.
The bundled set is deliberately closed (same policy as memory providers): Bitwarden and 1Password ship in-tree. Everything else — Infisical, Proton Pass, HashiCorp Vault, AWS Secrets Manager, OS keystores — belongs in plugin repos; share them in the Nous Research Discord (#plugins-skills-and-skins).