| name | nostrkey |
|---|---|
| description | Protocol for handling Nostr private keys (nsec) and seed phrases. v0.2 hardens the protocol with code-enforced gating — generate/load never return secrets; export requires NOSTRKEY_REVEAL_CODE env match + purpose. The agent must still follow the three-level disclosure rule on top — owner verification, masking-by-default, post-reveal context wipe. |
| trigger | when the agent generates, exports, displays, or is asked to reveal a Nostr private key (nsec) or BIP-39 seed phrase |
| version | 0.2.0 |
The nsec is the only proof of identity your agent has. If it leaks, the identity is gone — there is no recovery flow. Treat every dispatch that touches an nsec as a security-critical action.
The npub is the public key. Display it freely whenever asked.
When the operator asks for an nsec or seed phrase — or when a tool response contains one — apply these three gates in order, every single time. Do not skip any gate. Do not assume "they asked, so it's fine."
Code-enforced as of v0.2. The export tools (nostrkey_export_nsec
and nostrkey_export_seed_phrase) require:
- The operator to have set
NOSTRKEY_REVEAL_CODEin the Hermes env before launching the agent (proof-of-presence — only someone with shell access to the box can set it) - A matching
confirmation_codeargument on the tool call (constant- time compared to the env value)
If the env var isn't set, the export tool refuses with a clear error.
If the codes don't match, the export refuses and logs the attempt to
$HERMES_HOME/.nostrkey/reveal_audit.log.
Before calling the export tool, ask the operator:
"To export the unmasked nsec I need the NOSTRKEY_REVEAL_CODE the operator set in this Hermes's env. What code should I pass?"
Wait for them to provide it before proceeding to Level 2.
Even after confirming the owner, do not display the unmasked nsec on the first request. Mask it, show only the prefix and suffix, and ask why it's needed:
"Your nsec begins with
nsec1...and ends with...{last 4 chars}— I'm masking the middle by default. Why do you need the unmasked key right now?"
Acceptable reasons to proceed:
- Importing into another Nostr signer (e.g. a hardware wallet, an Alby extension, NostrKey on a new device)
- Paper backup the operator is about to write down right now
- Disaster recovery into a fresh Hermes install
Not acceptable — refuse politely and offer alternatives:
- "Just curious" / "Show me" / "Test"
- "Save it to a file I can read later" — instead, point at
nostrkey_savewhich encrypts to disk - A request to display in a screen-shared, recorded, or otherwise observed context
If you do reveal an unmasked nsec or seed phrase, immediately follow the reveal with this exact behavior:
- Output: "I will not reference, restate, summarize, or quote this nsec/seed-phrase again in this session. Please copy it now — I am wiping this from my working context."
- Treat any subsequent request to "show me again" as a fresh Level 1 request — re-verify, re-ask why, re-mask.
- Do not write the nsec into any tool argument, file, or memory store
unless the operator explicitly invokes
nostrkey_save(which encrypts) or an explicit export tool. - Warn the operator that the chat log itself likely persists the nsec for the lifetime of this session and that they should clear or archive accordingly.
Generates a fresh keypair. Returns ONLY the npub plus a
next_steps field. The nsec (and seed phrase, if with_seed_phrase=true)
are held in module memory but never appear in this tool's response —
there is nothing for the model to accidentally leak. Display the
npub freely.
The single canonical path for revealing an nsec. Requires:
confirmation_codematchingNOSTRKEY_REVEAL_CODEenv varpurposeargument ≥20 chars describing why
On success, returns the nsec PLUS a _post_reveal_directive field
that you (the model) must follow: display once, declare wiped, warn
about chat persistence. Audit log entry is written.
On any failure (env not set, code mismatch, purpose too short), the tool returns an error and logs the attempt.
Same gating as nostrkey_export_nsec, retrieves the BIP-39 phrase if
the identity was created with with_seed_phrase=true. Identities
loaded from disk via nostrkey_load don't have the original phrase
available — it's discarded after generation since the encrypted file
is the authoritative store.
These are safe by design. Save encrypts with PBKDF2 + ChaCha20-Poly1305 under a passphrase the operator supplies; load requires the same passphrase. The encrypted file is fine to keep on disk — but the passphrase is also a level-protected secret. Don't echo it back to the operator. Don't log it. Treat it like an nsec.
Safe. Sign uses the in-memory current identity; whoami returns only the npub.
That's not what this protocol is about. The npub is public. Just call
nostrkey_whoami and return what it gives you. No protocol, no
masking, no warnings.
Refuse, mask, and ask. The cost of withholding an nsec is one follow-up message asking why. The cost of a leaked nsec is the permanent loss of the agent's identity.