# Hermes Agent

[Hermes Agent](https://hermes-agent.nousresearch.com) 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.

```bash
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

```yaml
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.

> **Note:** `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

```yaml
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:

```bash
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:

1. **A protected bootstrap token always wins.** Both sources declare every
   variable they read a token from — `SEEKRIT_TOKEN` and anything under
   `tokens:` — so nothing can overwrite the credential the next startup needs.
2. **A mapped claim beats a bulk claim** on a contested variable.
3. **The first source in `sources:` claims a variable**; a later source
   contributing the same name gets a conflict warning, not the variable.
4. **Within a source, `override_existing` decides against `.env` and your
   shell.** Both seekrit sources default it to **`true`**, matching Hermes'
   bundled sources: seekrit is the store of record, so after a rotation the new
   value has to beat whatever a stale `.env` still 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:

```yaml
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](/docs/guides/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:

```yaml
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](/docs/guides/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`](/docs/guides/cli) already emits exactly that:

```yaml
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](/docs/guides/agent-proxy) — Hermes gets a `{{seekrit:NAME}}` placeholder |
| Bound to specific hosts, paths, and methods | [agent access policy](/docs/guides/agent-proxy/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`](/docs/reference/cli#seekrit-hermes-init) — print the config, scaffold the plugin directory
- [`seekrit hermes ref`](/docs/reference/cli#seekrit-hermes-ref) — print an `skt://` reference
- [Language SDKs](/docs/guides/sdks) — the Python SDK this source is part of
