nostr-auth (NIP-07)
Authenticate to a service using Nostr sign-in entirely client-side.
This is auth-only: the service hands you a challenge (string or event
template), you sign a kind-22242 AUTH event with a secp256k1 (BIP-340) key
and return the signature. No wallet, no browser extension, no publishing.
When to use
- A site shows "Sign in with Nostr" / asks
window.nostr.signEvent(...).
- An API hands you a challenge string or event template to sign.
- You must NOT publish a real note to a relay; signing is local.
Command
Run with Node (zero npm dependencies). The script lives at:
<skill_dir>/nostr_auth.js
# Sign a challenge (classic kind-22242 sign-in event):
node <skill_dir>/nostr_auth.js nip07 --challenge "<hex>" --domain example.com --callback https://example.com/verify
# Sign an arbitrary NIP-07-style event template:
node <skill_dir>/nostr_auth.js nip07 '{"kind":22242,"tags":[["challenge","<hex>"]],"content":""}'
# Print the identity a service would see:
node <skill_dir>/nostr_auth.js nip07 pubkey --domain example.com
Options
| Option |
Description |
--event <json> |
Event template JSON (kind, tags, content) |
--challenge <str> |
Sign-in challenge; builds a kind-22242 AUTH event |
--relay <url> |
Relay tag for the event / domain hint |
--domain <d> |
Service domain for key derivation |
--callback <url> |
POST the signed event ({"event":...}) and print the verdict |
--dry-run |
Sign but do not submit |
--json |
Machine-readable JSON output |
--key <hex> |
64-char hex private key as master secret |
--keyfile <path> |
Keyfile (default ~/.config/nostr-auth/master.key) |
--keyout <path> |
Where to persist a freshly generated master secret |
--generate |
Force a new master secret, overwriting the keyfile |
--single-key |
One global identity for all services (no per-domain derivation) |
-v, -q, -h, --version |
Verbose, quiet, help, version |
Key management (default, privacy-preserving)
- On first use a 32-byte master secret is generated at
~/.config/nostr-auth/master.key (mode 0600).
- Per service,
linkingPriv = HMAC-SHA256(master, serviceDomain).
- Same service → same identity (returning user). Different services →
different identities (no cross-service correlation).
--single-key shares one identity everywhere (less private).
Protocol flow
- Service provides a challenge or event template (kind 22242 by default).
- Event id:
sha256(JSON.stringify([0, pubkey, created_at, kind, tags, content])).
- Sign the raw 32-byte id with BIP-340 schnorr (secp256k1);
event.sig hex.
- POST
{"event": ...} to the callback; server replies {"status":"OK"} or
{"status":"ERROR","reason":"..."}.
References: https://github.com/nostr-protocol/nips/blob/master/07.md
Exit codes
| Code |
Meaning |
0 |
Login accepted ({"status":"OK"}) or operation completed |
1 |
Client-side error (bad event, invalid key, network failure) |
2 |
Usage error (no method/args, unknown option) |
3 |
Server responded {"status":"ERROR","reason":"..."} |
4 |
Non-200 HTTP status or non-JSON body from the callback |
Examples
Basic sign-in
node <skill_dir>/nostr_auth.js nip07 --challenge "012345..." --relay "wss://relay.example.com" --callback "https://site.example.com/verify"
Dry-run first (inspect without submitting)
node <skill_dir>/nostr_auth.js nip07 --challenge "012345..." --dry-run --json
Use a specific key
node <skill_dir>/nostr_auth.js nip07 '<template-json>' --key <64-char-hex-private-key>
Dependencies
- Node.js v20.19+ (v22+ recommended).
- Zero npm dependencies: pure BigInt secp256k1/schnorr +
node:crypto
(sha256/HMAC). Boots from a clean clone, no npm install needed at runtime.
Self-test (offline, cost-free)
cd <skill_dir>
npm test
A local mock "Sign in with Nostr" server validates the full
challenge → sign → submit → verify roundtrip, replay rejection, dry-run
safety and per-domain key stability.
Limitations
- Signs challenges / events locally; account creation depends on the remote
service recognizing the derived public key.
- The derived identity is an agent identity — not the same key a user's
browser extension would produce.
- Method surface: NIP-07-style sign-in only in v1.0.0. NIP-98 (HTTP Auth),
NIP-42 (relay AUTH) and NIP-05 are on the roadmap.
1---2name: nostr-auth3description: Nostr sign-in (NIP-07) for LLM coding agents (Claude Code, OpenCode, OpenClaw, Codex, Cursor, ...). Auth-only: signs sign-in challenge events with a secp256k1 (BIP-340) key derived from a local master secret and submits them to the service callback. No wallet, no extension, no relay account. Use whenever a site or API asks for a signed Nostr event or a Sign in with Nostr challenge. More methods coming soon: NIP-98, NIP-42, NIP-05.4---56# nostr-auth (NIP-07)78Authenticate to a service using **Nostr sign-in** entirely client-side.9This is **auth-only**: the service hands you a challenge (string or event10template), you sign a kind-22242 AUTH event with a secp256k1 (BIP-340) key11and return the signature. No wallet, no browser extension, no publishing.1213## When to use1415- A site shows "Sign in with Nostr" / asks `window.nostr.signEvent(...)`.16- An API hands you a challenge string or event template to sign.17- You must NOT publish a real note to a relay; signing is local.1819## Command2021Run with Node (zero npm dependencies). The script lives at:2223```24<skill_dir>/nostr_auth.js25```2627```bash28# Sign a challenge (classic kind-22242 sign-in event):29node <skill_dir>/nostr_auth.js nip07 --challenge "<hex>" --domain example.com --callback https://example.com/verify3031# Sign an arbitrary NIP-07-style event template:32node <skill_dir>/nostr_auth.js nip07 '{"kind":22242,"tags":[["challenge","<hex>"]],"content":""}'3334# Print the identity a service would see:35node <skill_dir>/nostr_auth.js nip07 pubkey --domain example.com36```3738## Options3940| Option | Description |41|---|---|42| `--event <json>` | Event template JSON (`kind`, `tags`, `content`) |43| `--challenge <str>` | Sign-in challenge; builds a kind-22242 AUTH event |44| `--relay <url>` | Relay tag for the event / domain hint |45| `--domain <d>` | Service domain for key derivation |46| `--callback <url>` | POST the signed event (`{"event":...}`) and print the verdict |47| `--dry-run` | Sign but do **not** submit |48| `--json` | Machine-readable JSON output |49| `--key <hex>` | 64-char hex private key as master secret |50| `--keyfile <path>` | Keyfile (default `~/.config/nostr-auth/master.key`) |51| `--keyout <path>` | Where to persist a freshly generated master secret |52| `--generate` | Force a new master secret, **overwriting** the keyfile |53| `--single-key` | One global identity for all services (no per-domain derivation) |54| `-v`, `-q`, `-h`, `--version` | Verbose, quiet, help, version |5556## Key management (default, privacy-preserving)5758- On first use a 32-byte **master secret** is generated at59 `~/.config/nostr-auth/master.key` (mode `0600`).60- Per service, `linkingPriv = HMAC-SHA256(master, serviceDomain)`.61- Same service → same identity (returning user). Different services →62 different identities (no cross-service correlation).63- `--single-key` shares one identity everywhere (less private).6465## Protocol flow66671. Service provides a challenge or event template (kind 22242 by default).682. Event id: `sha256(JSON.stringify([0, pubkey, created_at, kind, tags, content]))`.693. Sign the raw 32-byte id with BIP-340 schnorr (secp256k1); `event.sig` hex.704. POST `{"event": ...}` to the callback; server replies `{"status":"OK"}` or71 `{"status":"ERROR","reason":"..."}`.7273References: https://github.com/nostr-protocol/nips/blob/master/07.md7475## Exit codes7677| Code | Meaning |78|---|---|79| `0` | Login accepted (`{"status":"OK"}`) or operation completed |80| `1` | Client-side error (bad event, invalid key, network failure) |81| `2` | Usage error (no method/args, unknown option) |82| `3` | Server responded `{"status":"ERROR","reason":"..."}` |83| `4` | Non-200 HTTP status or non-JSON body from the callback |8485## Examples8687### Basic sign-in8889```bash90node <skill_dir>/nostr_auth.js nip07 --challenge "012345..." --relay "wss://relay.example.com" --callback "https://site.example.com/verify"91```9293### Dry-run first (inspect without submitting)9495```bash96node <skill_dir>/nostr_auth.js nip07 --challenge "012345..." --dry-run --json97```9899### Use a specific key100101```bash102node <skill_dir>/nostr_auth.js nip07 '<template-json>' --key <64-char-hex-private-key>103```104105## Dependencies106107- **Node.js** v20.19+ (v22+ recommended).108- **Zero npm dependencies**: pure BigInt secp256k1/schnorr + `node:crypto`109 (sha256/HMAC). Boots from a clean clone, no `npm install` needed at runtime.110111## Self-test (offline, cost-free)112113```bash114cd <skill_dir>115npm test116```117118A local mock "Sign in with Nostr" server validates the full119challenge → sign → submit → verify roundtrip, replay rejection, dry-run120safety and per-domain key stability.121122## Limitations123124- Signs challenges / events locally; account creation depends on the remote125 service recognizing the derived public key.126- The derived identity is an agent identity — not the same key a user's127 browser extension would produce.128- Method surface: NIP-07-style sign-in only in v1.0.0. NIP-98 (HTTP Auth),129 NIP-42 (relay AUTH) and NIP-05 are on the roadmap.