I started building my own Nostr signer for a simple reason: I wanted to understand what actually happens between a Nostr client and my private key. I didn't want signing to remain a black box.
That small experiment became a browser extension I called SkySigner.
The first version was simple.
Keep the private key local. Implement the basic NIP-07 interface. Let Nostr clients request my public key and ask SkySigner to sign events without ever giving the client access to the private key.
getPublicKey() worked.
signEvent() worked.
I imported my existing Nostr identity, verified locally that the private key derived the expected npub, and eventually used SkySigner with my real identity to publish and reply on Nostr.
So far, so good.
Then I tried messaging.
And that's where the experiment became much more interesting.
The signer started fighting the user
Modern Nostr messaging involves encryption operations such as NIP-44.
After adding NIP-44 encrypt/decrypt support, I started testing different Nostr clients.
Some clients generated many signer requests while loading messages.
This makes sense technically. A client may need to decrypt multiple messages when opening a conversation.
But imagine a signer asking:
Approve?
Approve?
Approve?
Approve?
...again and again just to read your messages.
At first this looks extremely secure.
Every operation requires consent.
But after clicking Approve twenty times, something changes.
You're probably no longer verifying anything.
You're just trying to make the popup disappear.
And that made me realize that excessive confirmation can become its own security problem.
So I changed the permission model
The current SkySigner experiment works roughly like this:
getPublicKey
→ session permission
NIP-44 encrypt/decrypt
→ session permission
known event kinds
→ configurable per site/session
unknown event kinds
→ always ask
every request/decision
→ local audit log
I also added popup-storm protection.
If the same client generates many requests for the same operation, SkySigner doesn't need to spawn a pile of approval windows. Requests can be queued and handled according to the permission granted to that site.
I then added site trust profiles and browser-session permissions.
For example, I can effectively tell a trusted Nostr client:
You may access my public key and perform these specific encryption operations during this browser session.
The private key still never leaves SkySigner.
But now we've arrived at another security problem.
What exactly are we trusting?
If every operation requires approval, the signer can become unusable.
If everything is automatically approved, the signer loses much of its purpose.
So perhaps the interesting security boundary isn't simply:
client vs private key
Maybe it is:
client + capability + event type + duration + user consent
A trusted client might be allowed to decrypt messages for one browser session.
A known event kind might receive a temporary permission.
An unusual or unknown event should still trigger explicit approval.
And everything should remain visible in a local audit log.
That's the model I'm experimenting with now.
SkySigner is still an experiment, not production-ready security software. Building it has been a way for me to understand Nostr signing, encryption, identity and permissions by actually implementing them instead of only reading about them.
And the experiment changed the question I started with.
Initially I was asking:
How do we keep the private key away from the Nostr client?
Now I'm also asking:
How do we prevent signer security from becoming “click Approve until the popup disappears”?
I'm especially interested in hearing from people who use or build Nostr signers.
How would you design this permission model? Which actions should a signer be able to remember for a trusted site or session, and which actions should always require explicit approval from the user?
Also, click here to read my article, “My Laptop Needs 8.6 Billion Years to Mine a Bitcoin Block”