All plugins

keychain

Keep Hermes provider credentials in the macOS login keychain instead of plaintext .env. Registers a SecretSource (secrets.keychain), so startup resolution runs through Hermes's own orchestrator, after ~/.hermes/.env loads and before credentials are read — the same path the bundled Bitwarden and 1Password sources use. Bindings are explicit (secrets.keychain.env: ENV_VAR: keychain-account) or discovered from an index the plugin keeps inside the keychain, so `hermes keychain put NAME` is the whole setup. Adds keychain_status (vault-backed vs still-plaintext inventory; names, lengths and outcomes only), keychain_migration_plan (report-only: lists candidates and returns the exact `hermes keychain migrate ...` command; writes nothing) and keychain_audit (fetch/migrate trail), plus a `hermes keychain put|ls|rm|test|migrate|status|audit` CLI. Migration is operator-only in that CLI: it stores each value, verifies it by readback, backs up .env (0600), then removes the variable through hermes_cli.config.remove_env_value() — the `hermes config unset` path, so core's .env lock and its refusals for package-managed installs and admin-managed keys still apply. Disclosure — reads and writes generic-password items in the user's login keychain through /usr/bin/security called by absolute path (service `hermes` by default); no network calls and no third-party service, nothing leaves the machine; the index of stored names lives in a keychain item (`hermes!index`) and the audit log in <hermes home>/plugin-data/keychain/audit.jsonl; the only .env write is the operator-run `hermes keychain migrate` described above; the agent-facing tools write nothing; a locked keychain or denied ACL is a warning, never a startup block; a stored value is passed on argv for the single `security add-generic-password` call (visible to same-user processes for that moment) while reads, the once-per-process hot path, never put a value on argv. macOS-only, no Python dependencies, no env vars.

shoaibyazdaniToolsv0.1.1Updated 1 hour ago

About

Keep Hermes provider credentials in the macOS login keychain instead of plaintext .env. Registers a SecretSource (secrets.keychain), so startup resolution runs through Hermes's own orchestrator, after ~/.hermes/.env loads and before credentials are read — the same path the bundled Bitwarden and 1Password sources use. Bindings are explicit (secrets.keychain.env: ENV_VAR: keychain-account) or discovered from an index the plugin keeps inside the keychain, so `hermes keychain put NAME` is the whole setup. Adds keychain_status (vault-backed vs still-plaintext inventory; names, lengths and outcomes only), keychain_migration_plan (report-only: lists candidates and returns the exact `hermes keychain migrate ...` command; writes nothing) and keychain_audit (fetch/migrate trail), plus a `hermes keychain put|ls|rm|test|migrate|status|audit` CLI. Migration is operator-only in that CLI: it stores each value, verifies it by readback, backs up .env (0600), then removes the variable through hermes_cli.config.remove_env_value() — the `hermes config unset` path, so core's .env lock and its refusals for package-managed installs and admin-managed keys still apply. Disclosure — reads and writes generic-password items in the user's login keychain through /usr/bin/security called by absolute path (service `hermes` by default); no network calls and no third-party service, nothing leaves the machine; the index of stored names lives in a keychain item (`hermes!index`) and the audit log in <hermes home>/plugin-data/keychain/audit.jsonl; the only .env write is the operator-run `hermes keychain migrate` described above; the agent-facing tools write nothing; a locked keychain or denied ACL is a warning, never a startup block; a stored value is passed on argv for the single `security add-generic-password` call (visible to same-user processes for that moment) while reads, the once-per-process hot path, never put a value on argv. macOS-only, no Python dependencies, no env vars.

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 edits config.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. security runs 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_status reports
    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 (the hermes config unset path), 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.
  • security is called by absolute path, never through PATH. It receives values on argv,
    so a shadowed binary earlier in PATH would collect every stored key. There is no
    shutil.which lookup: /usr/bin/security or nothing.
  • One honest caveat. security add-generic-password takes the secret on argv, so for the
    microseconds it runs the value is visible to same-user processes (ps). Storing via
    put/migrate is 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.

Developer Mode

Explore built-in Hermes toolsets and actions.

HermesAgent

Developer?

Create a Hermes extension for Desktop or agent workflows.

Developer Section