Key signer for macOS and Linux.
NoorSigner keeps your Nostr private keys safe. It runs in the background and signs messages for your Nostr apps - your keys never leave your computer.
Works standalone with any Nostr client that supports external signers. Battle-tested with NoorNote.
Step 1: Add your first account
./noorsigner add-accountYou'll be asked for:
- Your nsec (private key) - input is hidden for security
- A password (8+ characters) - used to encrypt your key
That's it! Your key is now safely stored.
Step 2: Start the daemon
./noorsigner daemonEnter your password when asked. The daemon will run in the background - you can close the terminal.
Your Nostr app can now use NoorSigner for signing!
Want to use multiple Nostr identities? Just add more accounts:
./noorsigner add-accountEach account has its own password.
See all your accounts:
./noorsigner list-accountsOutput shows which account is active (*):
Stored accounts:
* npub1abc... (active)
npub1def...
Total: 2 account(s)
Switch to a different account:
./noorsigner switch npub1def...Enter the password for that account. If the daemon is running, restart it to use the new account.
./noorsigner remove-account npub1def...You'll need to enter the account's password to confirm.
Once set up, just start the daemon:
./noorsigner daemon- If you used NoorSigner in the last 24 hours: No password needed!
- After 24 hours or a reboot: Enter your password once
The daemon stays running in the background. Your Nostr app handles the rest.
| Command | What it does |
|---|---|
add-account |
Add a new Nostr account |
list-accounts |
Show all accounts |
switch <npub> |
Switch to another account |
remove-account <npub> |
Delete an account |
daemon |
Start the background signer |
The following sections are for developers and advanced users.
- 🔐 Secure Key Storage: NIP-49 compatible scrypt encryption
- 👥 Multi-Account Support: Manage multiple Nostr identities
- 🛡️ Trust Mode: 24-hour authentication caching per account
- 🔑 NIP-44 & NIP-04: Encryption/decryption for DMs
- 🔌 Unix Socket IPC: Fast, secure local communication
- 🔒 Memory Safety: Keys cleared from memory after use
- 🔄 Background Daemon: Fork-based process isolation
- 🚀 Live Account Switching: Switch accounts without restarting daemon
./noorsigner add-accountThis will:
- Prompt for your nsec (private key, hidden input)
- Ask for an encryption password (8+ characters)
- Save encrypted key to
~/.noorsigner/accounts/<npub>/keys.encrypted - Set this as the active account
Note: noorsigner init is an alias for add-account when no accounts exist.
./noorsigner daemonThis will:
- Prompt for your encryption password (if Trust Mode expired)
- Create a Trust Mode session (24 hours)
- Fork to background
- Create Unix socket at
~/.noorsigner/noorsigner.sock
Your Nostr client can now communicate with the daemon via the Unix socket.
# Add a new account
noorsigner add-account
# List all accounts (* = active)
noorsigner list-accounts
# Switch to a different account
noorsigner switch <npub>
# Remove an account (requires password confirmation)
noorsigner remove-account <npub>
# Initialize (alias for add-account, first account only)
noorsigner init# Start the signing daemon
noorsigner daemon# Sign event with stored key (requires password)
noorsigner sign
# Test signing via daemon
noorsigner test-daemon
# Test signing with direct nsec input
noorsigner test <nsec>~/.noorsigner/
├── accounts/
│ ├── npub1abc.../
│ │ ├── keys.encrypted # Encrypted nsec
│ │ └── trust_session # 24h password cache
│ └── npub1def.../
│ ├── keys.encrypted
│ └── trust_session
├── active_account # Currently active npub
└── noorsigner.sock # Daemon socket (shared)
- Each account has its own directory under
accounts/ - Each account has separate encryption password
- Each account has its own Trust Mode session
- One daemon instance serves all accounts
- Live account switching via API (password required)
When upgrading from an older single-account NoorSigner:
- Run any command (e.g.,
noorsigner daemon) - Enter your password when prompted
- Old key is migrated to new structure automatically
- Old files are removed after successful migration
Transport: Unix Domain Socket (JSON newline-delimited)
Socket Path: ~/.noorsigner/noorsigner.sock
Request Format:
{
"id": "unique-request-id",
"method": "method_name",
...additional fields per method
}Get the public key (npub) of the currently active account.
Request:
{
"id": "req-001",
"method": "get_npub"
}Response:
{
"id": "req-001",
"signature": "npub1..."
}Sign a Nostr event (NIP-01).
Request:
{
"id": "req-002",
"method": "sign_event",
"event_json": "{\"content\":\"Hello\",\"kind\":1,\"tags\":[],\"created_at\":1234567890}"
}Response:
{
"id": "req-002",
"signature": "hex-schnorr-signature"
}Encrypt plaintext using NIP-44 (modern encryption).
Request:
{
"id": "req-003",
"method": "nip44_encrypt",
"plaintext": "Secret message",
"recipient_pubkey": "hex-pubkey-of-recipient"
}Response:
{
"id": "req-003",
"signature": "encrypted-payload"
}Decrypt NIP-44 encrypted payload.
Request:
{
"id": "req-004",
"method": "nip44_decrypt",
"payload": "encrypted-payload",
"sender_pubkey": "hex-pubkey-of-sender"
}Response:
{
"id": "req-004",
"signature": "Decrypted message"
}Encrypt plaintext using NIP-04 (deprecated but widely compatible).
Request:
{
"id": "req-005",
"method": "nip04_encrypt",
"plaintext": "Secret message",
"recipient_pubkey": "hex-pubkey-of-recipient"
}Response:
{
"id": "req-005",
"signature": "encrypted-payload"
}Decrypt NIP-04 encrypted payload.
Request:
{
"id": "req-006",
"method": "nip04_decrypt",
"payload": "encrypted-payload",
"sender_pubkey": "hex-pubkey-of-sender"
}Response:
{
"id": "req-006",
"signature": "Decrypted message"
}List all stored accounts with their metadata.
Request:
{
"id": "req-010",
"method": "list_accounts"
}Response:
{
"id": "req-010",
"accounts": [
{
"pubkey": "abc123...",
"npub": "npub1abc...",
"created_at": 1234567890
},
{
"pubkey": "def456...",
"npub": "npub1def...",
"created_at": 1234567891
}
],
"active_pubkey": "abc123..."
}Add a new account to the daemon.
Request:
{
"id": "req-011",
"method": "add_account",
"nsec": "nsec1...",
"password": "encryption-password",
"set_active": true
}Response:
{
"id": "req-011",
"success": true,
"pubkey": "abc123...",
"npub": "npub1abc..."
}Error Response:
{
"id": "req-011",
"success": false,
"error": "account already exists"
}Switch to a different account (loads new key into memory).
Request (by pubkey):
{
"id": "req-012",
"method": "switch_account",
"pubkey": "def456...",
"password": "password-for-target-account"
}Request (by npub):
{
"id": "req-012",
"method": "switch_account",
"npub": "npub1def...",
"password": "password-for-target-account"
}Response:
{
"id": "req-012",
"success": true,
"pubkey": "def456...",
"npub": "npub1def..."
}Remove an account from storage.
Request:
{
"id": "req-013",
"method": "remove_account",
"pubkey": "def456...",
"password": "password-for-this-account"
}Response:
{
"id": "req-013",
"success": true
}Error (cannot remove active account):
{
"id": "req-013",
"error": "cannot remove active account - switch to another account first"
}Get currently active account info.
Request:
{
"id": "req-014",
"method": "get_active_account"
}Response:
{
"id": "req-014",
"pubkey": "abc123...",
"npub": "npub1abc...",
"is_unlocked": true
}Gracefully shutdown the daemon.
Request:
{
"id": "req-020",
"method": "shutdown_daemon"
}Response:
{
"id": "req-020",
"signature": "success"
}Enable daemon autostart on system boot.
Request:
{
"id": "req-021",
"method": "enable_autostart"
}Response:
{
"id": "req-021",
"signature": "success"
}Disable daemon autostart.
Request:
{
"id": "req-022",
"method": "disable_autostart"
}Response:
{
"id": "req-022",
"signature": "success"
}Check if autostart is enabled.
Request:
{
"id": "req-023",
"method": "get_autostart_status"
}Response:
{
"id": "req-023",
"signature": "enabled"
}or
{
"id": "req-023",
"signature": "disabled"
}import * as net from 'net';
import * as os from 'os';
import * as path from 'path';
const socketPath = path.join(os.homedir(), '.noorsigner', 'noorsigner.sock');
function sendRequest(method: string, params: Record<string, any> = {}): Promise<any> {
return new Promise((resolve, reject) => {
const client = net.createConnection(socketPath);
const request = {
id: `req-${Date.now()}`,
method,
...params
};
client.on('connect', () => {
client.write(JSON.stringify(request) + '\n');
});
client.on('data', (data) => {
const response = JSON.parse(data.toString());
client.end();
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
});
client.on('error', reject);
});
}
// Get current account
const active = await sendRequest('get_active_account');
console.log('Active account:', active.npub);
// List all accounts
const list = await sendRequest('list_accounts');
console.log('Accounts:', list.accounts.length);
// Switch account
const switched = await sendRequest('switch_account', {
pubkey: 'target-pubkey-hex',
password: 'password-for-target'
});
// Sign event
const signed = await sendRequest('sign_event', {
event_json: JSON.stringify({
content: 'Hello Nostr',
kind: 1,
tags: [],
created_at: Math.floor(Date.now() / 1000)
})
});
console.log('Signature:', signed.signature);- Key Storage: NIP-49 compatible scrypt encryption (N=16384, r=8, p=1)
- Per-Account Passwords: Each account has its own encryption password
- Trust Mode: Session token encrypted with random 32-byte key
- Memory Safety: Keys zeroed out after use and on account switch
When daemon starts or switches accounts:
- Caches the decrypted nsec encrypted with a random session token
- Expires after 24 hours from creation
- Stored in account-specific
trust_sessionfile - Allows daemon to restart without password re-entry (within 24h)
Security Trade-off: Trust Mode trades security for convenience. Only use on devices you trust.
The Unix socket is created with 0600 permissions (owner read/write only), preventing other users from accessing it.
- Old private key is zeroed from memory before loading new key
- New account requires password verification
- New Trust Mode session created for switched account
- Socket path:
~/.noorsigner/noorsigner.sock - Autostart: LaunchAgent (
~/Library/LaunchAgents/com.noorsigner.daemon.plist) - Daemon launches via Terminal.app when called from GUI
- Socket path:
~/.noorsigner/noorsigner.sock - Autostart: XDG Autostart (
~/.config/autostart/noorsigner.desktop) - Same daemon behavior as macOS
# Clone repository
git clone https://gitlab.com/77elements/noorsigner.git
cd noorsigner
# Build for current platform
go build -o noorsigner .
# Or build all platforms
./build.sh
# Binaries will be in ./bin/- Check if socket already exists:
ls -la ~/.noorsigner/noorsigner.sock - Remove stale socket:
rm ~/.noorsigner/noorsigner.sock - Check for running processes:
ps aux | grep noorsigner - Kill existing daemon:
pkill noorsigner
- Daemon might not be running
- Socket file might not exist
- Check socket path matches your platform
- Verify socket permissions:
ls -la ~/.noorsigner/
- Encryption password is wrong
- Key file might be corrupted
- Use correct password for the specific account
- Account was removed or never created
- Use
list-accountsto see available accounts - Use
add-accountto create a new account
- This is expected! Trust Mode sessions expire after 24 hours OR system reboot
- Simply restart daemon and enter password again
MIT License - See LICENSE file for details
Contributions welcome! Please:
- Follow Go best practices
- Maintain backwards compatibility with existing clients
- Add tests for new features
- Update this README with API changes
- Multi-account support
- Live account switching via API
- NIP-44 encryption/decryption
- NIP-04 encryption/decryption
- Auto-launch on system startup (macOS/Linux)
- NIP-46 Remote Signer support
- Hardware wallet integration
- Custom Trust Mode duration
- GUI password prompt option
For issues, feature requests, or questions:
- GitLab Issues: Project Issues
- Nostr: Contact the maintainers on Nostr
Made with ⚡ for the Nostr ecosystem