hermes-vault-secret-source
A HashiCorp Vault secret source for Hermes Agent, packaged as a standalone pip plugin.
At Hermes startup it reads one Vault KV v2 secret path and injects every key found there as an environment variable, the same way the bundled Bitwarden Secrets Manager source injects a whole project. This is a bulk source, so explicit per-variable bindings from mapped sources (like 1Password) take precedence over it.
Install
pip install hermes-vault-secret-source
This pulls in hvac, the official Python client
for Vault. Install it into the same environment as hermes-agent.
To track the latest unreleased code, install from the repository instead:
pip install "hermes-vault-secret-source @ git+https://github.com/cryptoyasenka/hermes-vault-secret-source"
Hermes discovers the plugin automatically through the hermes_agent.plugins
entry point, but plugins are opt-in. Enable it once:
hermes plugins enable hermes-hashicorp-vault
Configure
Add a vault block under secrets in your Hermes config:
secrets:
vault:
enabled: true
# addr is read from $VAULT_ADDR by default; set `addr` to hard-code it.
path: apps/my-agent # KV v2 path whose keys become env vars
mount_point: secret # KV v2 mount (default: secret)
auth_method: token # "token" (default) or "approle"
cache_ttl_seconds: 300 # 0 disables caching
Credentials are never written into config; they come from environment variables:
| Auth method | Env vars used |
|---|---|
token |
VAULT_ADDR, VAULT_TOKEN |
approle |
VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID |
The env-var names are overridable per field (addr_env, token_env,
role_id_env, secret_id_env) if your deployment uses different names.
All config keys
| Key | Default | Meaning |
|---|---|---|
enabled |
false |
Master switch |
addr |
"" |
Vault URL; overrides addr_env when set |
addr_env |
VAULT_ADDR |
Env var holding the Vault URL |
path |
"" |
KV v2 path to read (required) |
mount_point |
secret |
KV v2 secrets-engine mount |
auth_method |
token |
token or approle |
token_env |
VAULT_TOKEN |
Env var with the token (token auth) |
role_id_env |
VAULT_ROLE_ID |
Env var with the role_id (approle) |
secret_id_env |
VAULT_SECRET_ID |
Env var with the secret_id (approle) |
namespace |
"" |
Vault Enterprise namespace |
verify |
true |
TLS verify: true/false or a CA-bundle path |
cache_ttl_seconds |
300 |
Disk+memory cache TTL; 0 disables caching |
request_timeout_seconds |
30 |
Per-request Vault HTTP timeout |
override_existing |
true |
Vault values overwrite existing env values |
How it behaves
- Never raises. A misconfigured or unreachable Vault produces a clean
FetchResultwith anErrorKind(not_configured,auth_failed,ref_invalid,network,timeout, ...), never a startup crash. - Protects its own credentials. The auth env vars (
VAULT_ADDR,VAULT_TOKEN,VAULT_ROLE_ID,VAULT_SECRET_ID) are marked protected, so a secret stored in Vault cannot clobber the credential used to reach Vault. - Caches values, never tokens. The optional on-disk cache stores only the resolved secret values; the token/secret_id are SHA-256 fingerprinted before they touch the cache key.
- Skips unsafe keys and values. A KV key that is not a valid environment
variable name, or one that could hijack how the process resolves binaries,
libraries, or interpreter startup (
PATH,LD_*,DYLD_*,PYTHONPATH, ...), is skipped with a warning rather than injected. Only scalar values (strings and numbers) are injected; a structured value (list or object) is skipped with a warning instead of being stringified into an unusable value.
Threat model
This plugin runs on the Hermes startup path with access to Vault credentials, so its security posture is deliberately narrow. Every protection below is already implemented; each maps to a concrete mechanism in the source.
Startup cannot be crashed by this source
fetch() never raises. Every configuration problem returns a FetchResult with
a machine-readable ErrorKind (not_configured, auth_failed, ref_invalid,
network, timeout, binary_missing, internal), and the backend in
_client.py only ever raises RuntimeError, which fetch() catches and
classifies. A misconfigured, unreachable, or unauthenticated Vault degrades to a
clean skip, never a traceback on the non-interactive startup path. Any other
unexpected error (for example a corrupt cache file surfaced by the shared cache
layer) is caught as well and reported as internal, so nothing can escape
fetch().
A Vault secret cannot hijack the credential used to reach Vault
protected_env_vars() marks the auth env vars (VAULT_ADDR, VAULT_TOKEN,
VAULT_ROLE_ID, VAULT_SECRET_ID, or their configured overrides) as protected.
The orchestrator will not overwrite a protected variable, so a key that happens
to be named VAULT_TOKEN inside the Vault payload cannot clobber the token the
process is already using to authenticate. Names that are not valid env-var
identifiers are dropped from the protected set, so a misconfigured section cannot
silently weaken it.
Tokens never reach the on-disk cache in the clear
The optional two-layer cache stores only resolved secret values. The Vault token
(or the AppRole role_id and secret_id) is SHA-256 fingerprinted by
_fingerprint() before it becomes part of the cache key, so neither the in-memory
key nor the vault_cache.json file on disk contains the raw credential. Caching
is opt-out: cache_ttl_seconds: 0 disables it entirely.
Only environment-safe keys are injected
Each KV v2 key is validated with the host's is_valid_env_name() before it is
contributed. A key that is not a valid environment-variable name is skipped with
a warning instead of being injected, so a stray Vault key cannot produce a
malformed process environment. Keys that are valid identifiers but could hijack
how the process resolves binaries, shared libraries, or interpreter startup
(PATH, LD_*, DYLD_*, PYTHONPATH, BASH_ENV, ...) are refused as well,
even when override_existing is on. Only scalar values are injected; a
structured value is skipped rather than stringified.
TLS verification is on by default
verify defaults to true. It is disabled only when the operator explicitly
sets a false-y value, and a string value is treated as a CA-bundle path. TLS
therefore fails closed rather than open.
Development
Requires a hermes-agent source checkout (for the agent.* packages and the
conformance kit at tests/secret_sources/conformance.py).
# host = hermes-agent checkout, plugin = this repo. Put the host on PYTHONPATH so
# `import tests` resolves to the host conformance package. This repo keeps its own
# tests in `checks/` (not `tests/`) precisely so it cannot shadow that package.
PYTHONPATH="/path/to/hermes-agent:$(pwd)" pytest
checks/test_conformance.py runs the host SecretSourceConformance contract;
checks/test_vault_source.py covers behavior with hvac mocked (no network, no
Vault binary needed).
To run against a real dev server:
vault server -dev # prints VAULT_ADDR + a root token
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=<root-token-from-output>
vault kv put secret/apps/my-agent API_KEY=abc DB_URL=postgres://x
License
MIT
Release files for hermes-vault-secret-source 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hermes_vault_secret_source-0.1.2.tar.gz | 15.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hermes_vault_secret_source-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.7 kB
Release files / hermes_vault_secret_source-0.1.2.tar.gz
| Download URL | hermes_vault_secret_source-0.1.2.tar.gz |
|---|---|
| Size | 15.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b1618c53f9e37d25677c9acffc33308f46dc54bf65ac5550a58504b2e411a877
|
|
BLAKE2b-256 checksum How to use checksums |
90151c7e933fe008b56d6f9a6676f7a341d04bee5fd06f2fb75235b06b933517
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.
Transparency logRelease files / hermes_vault_secret_source-0.1.2-py3-none-any.whl
| Download URL | hermes_vault_secret_source-0.1.2-py3-none-any.whl |
|---|---|
| Size | 13.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f79d88eace01ca7bb4e8e583000aeb8e648bb7e76a8fdc1db33c861033fc91cf
|
|
BLAKE2b-256 checksum How to use checksums |
819249f68da84a46a41e0a4b289985c02aace5df4184696adcd79f7983f89e5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.
Transparency log