Hermes Agent
Hermes Agent reads its credentials —
model providers, tool APIs, messaging platforms — from ~/.hermes/.env and the
process environment. A secret source fills that environment from somewhere
else at startup, before Hermes reads any of it, and that is where seekrit fits.
pip install seekrit
hermes plugins enable seekrit
seekrit hermes init
init prints the block to merge into ~/.hermes/config.yaml, plus the two steps
around it. Once it is in place, .env holds one thing — the service token — and
every other credential arrives decrypted at startup.
Two sources, and the choice matters
pip install seekrit registers both. Neither does anything until a config
section enables it.
| Source | Shape | Use it when |
|---|---|---|
seekrit | bulk — one environment, whole | The usual case. Nothing to enumerate, and a secret added later appears without a config edit. |
seekrit_refs | mapped — explicit VAR: skt://NAME | You need to rename a secret, take only some of an environment, read more than one environment, or win a contested variable. |
The distinction is not cosmetic: Hermes lets a mapped claim beat a bulk one on a variable both sources offer. If seekrit is your store of record and you also run another source, mapped is how you make sure seekrit wins.
Bulk
secrets:
sources: [seekrit]
seekrit:
enabled: true
Optional keys: include and exclude narrow which names are contributed,
token_env names a different variable to read the token from, overrides pulls
a different environment slice of a composed group, api_url and
timeout_seconds do what they say.
include and exclude are a tidiness tool, not a boundary. The token still
resolves the whole environment, so a secret that must not reach this process
needs a narrower token — not an exclude.
Mapped
secrets:
sources: [seekrit_refs]
seekrit_refs:
enabled: true
tokens:
billing: SEEKRIT_TOKEN_BILLING
env:
OPENAI_API_KEY: skt://OPENAI_KEY
STRIPE_SECRET_KEY: skt://billing/STRIPE_SECRET_KEY
A reference is skt://NAME, or skt://<token-alias>/NAME to resolve through a
second token. A tokens: entry names an environment variable, never a token
value — config.yaml is not a place for a credential, and Hermes' own docs say
the same.
seekrit hermes init will write the whole block for you:
seekrit hermes init --mapped \
--bind OPENAI_API_KEY=OPENAI_KEY \
--bind STRIPE_SECRET_KEY=billing/STRIPE_SECRET_KEY \
--token billing=SEEKRIT_TOKEN_BILLING
Why a reference names a token and not an app and environment: a skt_ service
token is bound to exactly one environment, so there is nothing on the read path
that could resolve an arbitrary app/env pair. An skt://app/env/NAME spelling
would be a reference that always failed.
Precedence
Hermes owns this, and it is worth knowing before you debug a value that is not what you expected. In order:
- A protected bootstrap token always wins. Both sources declare every
variable they read a token from —
SEEKRIT_TOKENand anything undertokens:— so nothing can overwrite the credential the next startup needs. - A mapped claim beats a bulk claim on a contested variable.
- The first source in
sources:claims a variable; a later source contributing the same name gets a conflict warning, not the variable. - Within a source,
override_existingdecides against.envand your shell. Both seekrit sources default it totrue, matching Hermes' bundled sources: seekrit is the store of record, so after a rotation the new value has to beat whatever a stale.envstill holds.
That last default is the one that surprises people. When a local override is the
point — a variable you set in your shell to test something — name it under
preserve_existing and it always wins:
secrets:
preserve_existing: [OPENAI_API_KEY]
The token
Put it in ~/.hermes/.env, which is what that file is for:
SEEKRIT_TOKEN=skt_…
Scope it to exactly the environment this agent needs — see service tokens. A Hermes agent has broad reach by design, so the token is the ceiling that matters.
When something fails
A secret source may not raise, so failures come back as an error kind that Hermes reports at startup with a remediation hint. What each means here:
| Kind | Means |
|---|---|
NOT_CONFIGURED | The source is enabled but SEEKRIT_TOKEN is not set — or, for the mapped source, env: is empty. |
AUTH_FAILED | seekrit rejected the token, or the token cannot decrypt this environment. Check its key grant with seekrit access list. |
REF_INVALID | A reference is malformed, names a token alias that is not under tokens:, or points at a secret that does not exist. |
NETWORK | The seekrit API was unreachable, rate-limited, or returned a 5xx. |
EMPTY_VALUE | The secret exists and holds an empty string. Neither source will apply "" over a working credential. |
TIMEOUT | Raise timeout_seconds for the source. |
A mapped source fails whole, not partially: a binding is an explicit statement that this variable comes from seekrit, so half a map is a config error worth surfacing at startup rather than an agent that mostly works.
Tools, as well as credentials
Filling the environment is one half. Hermes also discovers MCP servers at startup and registers their tools alongside its built-ins, so the agent can manage secrets as well as use them:
mcp_servers:
seekrit:
command: npx
args: ["-y", "@seekrit/mcp"]
seekrit-cloud:
url: https://mcp.seekrit.dev/mcp
The local server holds your key and does the crypto; the hosted one is zero-knowledge and never returns a value. See AI agents.
Without the plugin
If installing a Python package into the Hermes environment is not an option,
Hermes ships a generic command source that reads KEY=VALUE lines from any
helper — and seekrit export already emits exactly that:
secrets:
sources: [command]
command:
enabled: true
command: ["seekrit", "export", "--format", "dotenv"]
This is a weaker arrangement than the real source: no per-variable provenance, no reference syntax, no protected bootstrap token, and a shell-out per startup. Use it as a stopgap.
What this does and does not protect
Filling the environment keeps plaintext out of ~/.hermes/.env, and takes the
credentials out of a file that gets copied into containers, backed up, and
occasionally pasted into a chat. It does not stop the agent from reading a
value: the secrets are in os.environ of the process the model drives, and
Hermes says as much about its own sources.
| Requirement | Use |
|---|---|
| No plaintext at rest on disk | this page |
| The agent never holds the key at all | egress proxy — Hermes gets a {{seekrit:NAME}} placeholder |
| Bound to specific hosts, paths, and methods | agent access policy |
The two compose well: resolve the proxy's own credential through the secret source, and give Hermes placeholders for every upstream behind it.
Troubleshooting
| Symptom | Cause |
|---|---|
| Hermes never mentions the source at startup | It is registered but not enabled — secrets.seekrit.enabled: true, and hermes plugins enable seekrit. |
HERMES_PLUGINS_DEBUG=1 shows no seekrit plugin | The seekrit package is not installed in the environment Hermes runs in. Scaffold the directory install instead: seekrit hermes init --plugin-dir. |
| A variable holds a stale value | Another source claimed it first, or preserve_existing names it. Check the (from …) provenance label Hermes prints. |
| Credentials work in the CLI but not in Hermes | Hermes runs from ~/.hermes, not your shell — the token must be in .env, not just exported in one terminal. |
Reference
seekrit hermes init— print the config, scaffold the plugin directoryseekrit hermes ref— print anskt://reference- Language SDKs — the Python SDK this source is part of