hermes-keychain
Your Hermes API keys belong in the OS keystore, not in a text file.
hermes-keychain makes the macOS Keychain a first-class Hermes Agent
secret source. Credentials are resolved from the login keychain at process startup — after
.env loads, before the first model call — so a stolen .env, a committed dotfile, a screen
share or a backup never contains a live key.
Built for the Nous Research Hermes Agent Plugin Hackathon (Nov 2026).
hermes keychain put OPENAI_API_KEY # prompts, no echo, verified readback
hermes keychain migrate # move every .env key into the keychain
hermes keychain status # what is vault-backed, what is still plaintext
Why this is a plugin and not a skill
Hermes's credential pipeline has a documented extension point — SecretSource — and a skill
cannot reach it. load_hermes_dotenv() loads ~/.hermes/.env, then the orchestrator
(agent.secret_sources.registry.apply_all) pulls every enabled source, resolves precedence
(mapped beats bulk, first-claim-wins, override_existing), and only then writes
os.environ. A plugin sits inside that sequence; a shell command or a skill can only run
before or after it.
The result is that Hermes resolves ANTHROPIC_API_KEY from the keystore with no wrapper
script, no op run, no exported shell profile — the same call path it uses for .env.
What you get
| Surface | Name | What it does |
|---|---|---|
| Secret source | secrets.keychain | Resolves bound credentials into the environment at startup |
| Tool | keychain_status | Vault-backed vs plaintext inventory, bindings, recent fetches |
| Tool | keychain_migration_plan | Report-only: lists what could move + the exact CLI command (writes nothing) |
| Tool | keychain_audit | The append-only fetch/migrate trail (names and outcomes, never values) |
| CLI | hermes keychain | put, ls, rm, test, migrate, status, audit |
Install
hermes plugins install shoaibyazdani/hermes-keychain
hermes keychain status # shows what is bound, what is still plaintext
hermes config set secrets.keychain.enabled true
Restart the gateway (or start a new hermes session) so startup resolution runs.
Store and migrate
Prompted, no echo, value verified by readback before the command reports success:
hermes keychain put OPENAI_API_KEY
Already have keys in ~/.hermes/.env? Move them — per key: store → read back → compare →
only then replace the .env line with a comment that names the variable. .env is backed
up to .env.bak-<utc> (mode 0600) first, unrelated lines and ordering are untouched, and a
failed store leaves .env alone.
hermes keychain migrate --dry-run # report only
hermes keychain migrate # credential-looking names
hermes keychain migrate OPENAI_API_KEY ANTHROPIC_API_KEY
From a session the agent can only plan: keychain_migration_plan lists the candidates and
hands you the command. It never stores a key and never edits .env. That asymmetry is
deliberate — a model choosing secret names and rewriting <hermes home>/.env unattended can
strand every provider key if the source is later disabled or the login keychain is locked at
the next startup. Migration stays with the human.
Then confirm resolution really happens without .env:
hermes keychain test OPENAI_API_KEY # ok (51 chars, service hermes)
hermes keychain audit --limit 10 # fetch OPENAI_API_KEY ok 71ms
Configuration
secrets:
keychain:
enabled: true
service: hermes # keychain service for items this plugin owns
discover: true # bind every indexed item whose name is env-shaped
override_existing: false # let the keychain win over .env / shell values
timeout_seconds: 8
env: # optional explicit bindings: ENV_VAR -> keychain account
ANTHROPIC_API_KEY: anthropic-prod
Two binding modes, both supported at once:
- Discovered (zero-config) — anything stored through this plugin is recorded in an index
kept inside the keychain (hermes!index). Every indexed name that looks like an
environment variable is bound to its own account. Store it once, restart, done. - Explicit — map a variable to an existing keychain account you already have (a
different service, a shared item, a name that is not env-shaped).
Security model
- Read-only source. It contributes values to Hermes's orchestrator and never writes
os.environ, never editsconfig.yaml, never prompts. - Fail-open startup. A locked keychain, a missing item, a denied ACL — every failure is
classified (not_found,auth_failed,timeout) and reported as a warning, so a broken
keychain cannot stop Hermes from starting.securityruns with stdin closed and a hard
timeout: an unattended run never hangs on a prompt. - Values never leave the keystore except into the process env. They are not logged, not
in the audit trail, not in tool output, not in error messages.keychain_statusreports
names, lengths and outcomes only. - Migration is operator-only, and removal is core's. Only
hermes keychain migrate— run
by you — stores and then removes a variable, and it removes through
hermes_cli.config.remove_env_value(thehermes config unsetpath), so core's.env
lock and its refusals for package-managed installs and admin-managed keys still apply. A
0600 timestamped backup is taken first, and nothing is removed until the keychain readback
for that name matched. The equivalent tool (keychain_migration_plan) is report-only. securityis called by absolute path, never throughPATH. It receives values on argv,
so a shadowed binary earlier inPATHwould collect every stored key. There is no
shutil.whichlookup:/usr/bin/securityor nothing.- One honest caveat.
security add-generic-passwordtakes the secret on argv, so for the
microseconds it runs the value is visible to same-user processes (ps). Storing via
put/migrateis a local, one-shot operation; reads — the hot path, once per process —
never put a value on argv. A Keychain Framework binding could remove even that window; it
is not worth a hard dependency on PyObjC here.
Platform
macOS only (/usr/bin/security). On any other platform the source reports
not_configured and the tools explain why — nothing else breaks. Linux/Windows users get
the same contract through the bundled command source (secret-tool, keepassxc-cli,
pass) without a plugin.
Tests
Pure logic (dotenv editing, index parsing, failure classification, binding discovery) is
covered by unit tests that never touch the real keychain:
python -m pytest # 15 tests
hermes plugins doctor . # runtime contracts: discovery, manifest, register()
hermes plugins validate . --install-deps # the catalog CI gate
License
MIT
README from the reviewed commit bde7866; it updates when the author re-pins.

shoaibyazdani