rotate-secrets — Atomic Secret Rotation
Rotate secrets in ~/.hermes/.env, propagate the new values to every service that consumes them, and restart only the affected gateways.
Procedure
Parse the pattern. Match against every key in ~/.hermes/.env. Support glob syntax (*, ?, [abc]) and the literal all.
For each matched key:
a. Determine the secret kind from the key name:
*_HMAC_* or *_WEBHOOK_SECRET → generate openssl rand -hex 32
*_API_KEY → prompt the user to provide the new value (can't auto-rotate external APIs)
GITHUB_*_TOKEN → open https://github.com/settings/tokens and prompt for new PAT
TWILIO_AUTH_TOKEN → direct user to rotate in Twilio console and prompt for new value
- Unknown pattern → prompt user for the kind
b. Back up the current .env as ~/.hermes/.env.bak.YYYYMMDDHHMMSS before any write.
c. Update the .env atomically. Don't build a sed s/// expression from
the key or value — secret values routinely contain /, &, and \,
which corrupt the substitution (and switching the delimiter to | just
moves the problem). Use exact-match rewrite instead:
tmp=$(mktemp ~/.hermes/.env.XXXXXX)
KEY="$KEY" NEW_VALUE="$NEW_VALUE" awk -F= '
$1 == ENVIRON["KEY"] { print ENVIRON["KEY"] "=" ENVIRON["NEW_VALUE"]; found=1; next }
{ print }
END { if (!found) print ENVIRON["KEY"] "=" ENVIRON["NEW_VALUE"] }
' ~/.hermes/.env > "$tmp" && chmod 600 "$tmp" && mv "$tmp" ~/.hermes/.env
This appends the key if it was missing, keeps 0600 perms, and never
interprets a secret as a regex.
Propagate to external services. For HMAC / webhook secrets, update the remote side:
- GitHub webhooks: use
github MCP to PATCH /repos/{owner}/{repo}/hooks/{hook_id} with config.secret
- Twilio: user-guided — we don't touch Twilio SMS webhook config automatically
- Slack: user-guided — rotate signing secret in App Manifest
- Discord: user-guided — rotate public key in Developer Portal
- Generic webhook: ask the user where the producer-side config lives
Restart the gateway. Platform-scoped restarts aren't a thing — one
gateway process serves every platform, so any token rotation is
followed by a single restart of the whole service:
hermes gateway restart # systemd/launchd service (or run hermes gateway run again if foreground)
- For the SMS/Twilio or any other adapter: same command — no per-platform flag exists.
Verify. Run hermes doctor and fail loud if any gateway is unhealthy post-rotation. If unhealthy, restore from the .env.bak.* backup and report.
Emit a rotation log entry. Append to ~/.hermes/logs/rotations.log:
2026-04-17T14:22:00Z rotated webhook_hmac_github by=user result=ok prev_sha=abc123 new_sha=def456
Store SHA-256 of the secret, never the plaintext.
Security notes
- Never log the plaintext new or old value.
- Never echo a secret into the Telegram/Discord channel where the rotation was requested. Approval prompts route to the originating channel (Part 19, Layer 2) — so only ever run rotations from your owner-only admin DM or the local CLI, never from a shared or public channel.
- For critical rotations (Anthropic, OpenAI, etc.), pause all gateways during rotation to prevent mid-flight requests hitting rejected keys.
- Back up
.env before every run; retain 30 days of backups.
Example invocation
/rotate-secrets webhook_hmac_*
/rotate-secrets TWILIO_AUTH_TOKEN
/rotate-secrets all # With interactive confirmation per key
Headless / cron use
Only the HMAC/webhook kinds are fully automatic — API keys and PATs prompt
the operator, and a prompt in a headless cron session never gets answered:
the run stalls until approvals.timeout (or the session's own timeout) kills
it. So:
- Cron
/rotate-secrets webhook_hmac_* — fine; nothing prompts.
- Cron
/rotate-secrets all — don't. It will hang on the first
interactive kind. Run all manually from your admin DM/CLI, monthly.
1---2name: rotate-secrets3description: Rotate webhook HMACs, API keys, OAuth tokens, and update gateway configs atomically4---56# rotate-secrets — Atomic Secret Rotation78Rotate secrets in `~/.hermes/.env`, propagate the new values to every service that consumes them, and restart only the affected gateways.910## Procedure11121. **Parse the pattern.** Match against every key in `~/.hermes/.env`. Support glob syntax (`*`, `?`, `[abc]`) and the literal `all`.13142. **For each matched key:**15 a. Determine the secret kind from the key name:16 - `*_HMAC_*` or `*_WEBHOOK_SECRET` → generate `openssl rand -hex 32`17 - `*_API_KEY` → prompt the user to provide the new value (can't auto-rotate external APIs)18 - `GITHUB_*_TOKEN` → open https://github.com/settings/tokens and prompt for new PAT19 - `TWILIO_AUTH_TOKEN` → direct user to rotate in Twilio console and prompt for new value20 - Unknown pattern → prompt user for the kind2122 b. Back up the current `.env` as `~/.hermes/.env.bak.YYYYMMDDHHMMSS` before any write.2324 c. Update the `.env` atomically. **Don't build a `sed s///` expression from25 the key or value** — secret values routinely contain `/`, `&`, and `\`,26 which corrupt the substitution (and switching the delimiter to `|` just27 moves the problem). Use exact-match rewrite instead:28 ```bash29 tmp=$(mktemp ~/.hermes/.env.XXXXXX)30 KEY="$KEY" NEW_VALUE="$NEW_VALUE" awk -F= '31 $1 == ENVIRON["KEY"] { print ENVIRON["KEY"] "=" ENVIRON["NEW_VALUE"]; found=1; next }32 { print }33 END { if (!found) print ENVIRON["KEY"] "=" ENVIRON["NEW_VALUE"] }34 ' ~/.hermes/.env > "$tmp" && chmod 600 "$tmp" && mv "$tmp" ~/.hermes/.env35 ```36 This appends the key if it was missing, keeps `0600` perms, and never37 interprets a secret as a regex.38393. **Propagate to external services.** For HMAC / webhook secrets, update the remote side:40 - **GitHub webhooks:** use `github` MCP to `PATCH /repos/{owner}/{repo}/hooks/{hook_id}` with `config.secret`41 - **Twilio:** user-guided — we don't touch Twilio SMS webhook config automatically42 - **Slack:** user-guided — rotate signing secret in App Manifest43 - **Discord:** user-guided — rotate public key in Developer Portal44 - **Generic webhook:** ask the user where the producer-side config lives45464. **Restart the gateway.** Platform-scoped restarts aren't a thing — one47 gateway process serves every platform, so any token rotation is48 followed by a single restart of the whole service:49 - `hermes gateway restart` # systemd/launchd service (or run `hermes gateway run` again if foreground)50 - For the SMS/Twilio or any other adapter: same command — no per-platform flag exists.51525. **Verify.** Run `hermes doctor` and fail loud if any gateway is unhealthy post-rotation. If unhealthy, restore from the `.env.bak.*` backup and report.53546. **Emit a rotation log entry.** Append to `~/.hermes/logs/rotations.log`:55 ```56 2026-04-17T14:22:00Z rotated webhook_hmac_github by=user result=ok prev_sha=abc123 new_sha=def45657 ```58 Store SHA-256 of the secret, never the plaintext.5960## Security notes6162- Never log the plaintext new or old value.63- Never echo a secret into the Telegram/Discord channel where the rotation was requested. Approval prompts route to the originating channel ([Part 19, Layer 2](../../../part19-security-playbook.md#layer-2-dangerous-command-approval)) — so only ever run rotations from your owner-only admin DM or the local CLI, never from a shared or public channel.64- For critical rotations (Anthropic, OpenAI, etc.), pause all gateways during rotation to prevent mid-flight requests hitting rejected keys.65- Back up `.env` before every run; retain 30 days of backups.6667## Example invocation6869```70/rotate-secrets webhook_hmac_*71/rotate-secrets TWILIO_AUTH_TOKEN72/rotate-secrets all # With interactive confirmation per key73```7475## Headless / cron use7677Only the HMAC/webhook kinds are fully automatic — API keys and PATs **prompt78the operator**, and a prompt in a headless cron session never gets answered:79the run stalls until `approvals.timeout` (or the session's own timeout) kills80it. So:8182- Cron `/rotate-secrets webhook_hmac_*` — fine; nothing prompts.83- Cron `/rotate-secrets all` — **don't.** It will hang on the first84 interactive kind. Run `all` manually from your admin DM/CLI, monthly.