/auth-signers
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract
$ npx -y skills add vitorpamplona/amethyst --skill auth-signers --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/auth-signers
Context preview
The summary Claude sees to decide when to auto-load this skill.
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract
SKILL.md
auth-signers.SKILL.mdname: auth-signers
description: Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract `NostrSigner` base class, `SignerResult` contract, how to wire a new flow that needs to sign events, and the security/UX trade-offs between signer kinds.
Auth & Signers
Any time Amethyst produces a signed Nostr event, it goes through a `NostrSigner`. There are three kinds; all three implement the same abstract contract so feature code doesn't care which one the user has configured.
When to Use This Skill
- Adding a new flow that publishes an event (follow, post, react, zap, profile edit).
- Reviewing whether a feature works when the user has a remote bunker signer or an external Android signer.
- Debugging "Sign request approved but nothing happens" / timeouts on sign operations.
- Onboarding a new signer kind (hardware signer, browser extension, etc.).
- Understanding the NIP-46 bunker request/response taxonomy.
The Abstract Contract
`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt`:
abstract class NostrSigner(val pubKey: HexKey) {
abstract fun <T : Event> sign(
template: EventTemplate<T>,
onReady: (T) -> Unit,
)
abstract fun nip04Encrypt(plaintext: String, toPubKey: HexKey, onReady: (String) -> Unit)
abstract fun nip04Decrypt(ciphertext: String, fromPubKey: HexKey, onReady: (String) -> Unit)
abstract fun nip44Encrypt(...)
abstract fun nip44Decrypt(...)
abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapRequestEvent) -> Unit)
}Sibling files in the same folder:
- **`NostrSignerInternal.kt`** — in-process signer with the user's seckey in memory. Fastest; used for locally-stored accounts.
- **`NostrSignerSync.kt`** — blocking wrapper for scripts / migrations / tests where callbacks are inconvenient.
- **`EventTemplate.kt`** — the unsigned holder passed to `sign()`.
- **`SignerExceptions.kt`** — the error taxonomy (user denied, timeout, unsupported method, etc.).
- **`caches/`** — request cache so duplicate sign/encrypt requests coalesce.
Concrete implementations
- **Local (in-process)**: `NostrSignerInternal` — direct `Secp256k1Instance.signSchnorr` + NIP-44 inline. Used by accounts created/imported into Amethyst.
- **Remote (NIP-46 bunker)**: `quartz/.../nip46RemoteSigner/signer/NostrSignerRemote.kt`. Talks to a bunker service over Nostr DMs using the `BunkerRequest*` / `BunkerResponse*` event taxonomy (`BunkerRequestConnect`, `BunkerRequestSign`, `BunkerRequestNip44Encrypt`, …).
- **Android external (NIP-55)**: `quartz/src/androidMain/.../nip55AndroidSigner/client/NostrSignerExternal.kt`. Uses Android intents + content provider to delegate to another app on the same device. Launcher: `ExternalSignerLogin.kt`, `IActivityLauncher.kt`. Install-check: `IsExternalSignerInstalled.kt`.
The `SignerResult` Contract
Signers return via callback (and internally track via `SignerResult` sealed types in `nip46RemoteSigner/signer/SignerResult.kt` and `nip55AndroidSigner/api/SignerResult.kt`). Result variants cover success, user-denied, timeout, remote-disconnected, unsupported. Feature code should:
1. Pass a callback that handles success. 2. Trust the cache/timeout behavior — don't roll your own retry. 3. Surface `SignerExceptions` to the user with actionable messaging (e.g. "Bunker disconnected — reconnect?").
Typical Flow (Feature Code)
// High-level: Account methods already do this internally.
val signer: NostrSigner = account.signer // whichever kind the user configured
val template = reactionEventTemplate(noteId, authorPubKey, "+")
signer.sign(template) { signed ->
account.sendToRelays(signed) // or similar pipeline
}Most feature code should go through `Account`'s mutation methods (`account.sendReaction`, `account.follow`) rather than touching the signer directly — the account layer handles signing + publishing + local state update atomically. Reach for the signer directly only when `Account` doesn't have a helper.
Choosing a Signer at Sign-Up
Entry points:
- **Existing private key** (`nsec`, 32-byte hex, file) → `NostrSignerInternal`.
- **Bunker URL** (`bunker://...`) → `NostrSignerRemote.fromBunkerUri(bunkerUri, localSigner, client)` in `nip46RemoteSigner/signer/NostrSignerRemote.kt` parses the URI and returns a `NostrSignerRemote`; then call its `suspend fun connect()` to perform the NIP-46 handshake.
- **Installed external signer app** (Amber, nos2x, etc. on Android) → `ExternalSignerLogin.launch(...)` opens the signer app; approval yields a `NostrSignerExternal`.
The UI hosts both flows via `amethyst/.../ui/screen/loggedOff/login/` — look there for `ExternalSignerButton.kt` and the bunker-URL paste screen.
Trade-offs
| Signer | Latency | Offline OK? | Security | UX | |--------|---------|-------------|----------|-----| | Internal | µs | Yes | Key in app memory | No confirmation prompts | | Remote (NIP-46) | 100ms–seconds | No (needs bunker reachable) | Key never touches Amethyst | Occasional approval prompts | | External (NIP-55) | 100–500ms | Yes | Key in separate app | Prompt on every sign by default (configurable) |
Gotchas
- **Callbacks may never fire.** External signers can be dismissed without result; remote signers can time out. Use `SignerExceptions` / timeout handling at every call site or rely on the `Account` layer's wrapping.
- **`nip04Encrypt` is legacy** for NIP-04 DMs. New DM code should use NIP-17 gift-wrap → `nip44Encrypt` path.
- **Don't cache signer output** beyond the `caches/` that quartz already maintains. Stale cache entries lead to duplicate publishes.
- **Remote signer disconnects** need explicit reconnection UX — `RemoteSignerManager` exposes state; hook int
Read more
name: auth-signers description: Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract `NostrSigner` base class, `SignerResult` contract, how to wire a new flow that needs to sign events, and the security/UX trade-offs between signer kinds.
Auth & Signers
Any time Amethyst produces a signed Nostr event, it goes through a `NostrSigner`. There are three kinds; all three implement the same abstract contract so feature code doesn't care which one the user has configured.
When to Use This Skill
- Adding a new flow that publishes an event (follow, post, react, zap, profile edit).
- Reviewing whether a feature works when the user has a remote bunker signer or an external Android signer.
- Debugging "Sign request approved but nothing happens" / timeouts on sign operations.
- Onboarding a new signer kind (hardware signer, browser extension, etc.).
- Understanding the NIP-46 bunker request/response taxonomy.
The Abstract Contract
`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt`:
abstract class NostrSigner(val pubKey: HexKey) {
abstract fun <T : Event> sign(
template: EventTemplate<T>,
onReady: (T) -> Unit,
)
abstract fun nip04Encrypt(plaintext: String, toPubKey: HexKey, onReady: (String) -> Unit)
abstract fun nip04Decrypt(ciphertext: String, fromPubKey: HexKey, onReady: (String) -> Unit)
abstract fun nip44Encrypt(...)
abstract fun nip44Decrypt(...)
abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapRequestEvent) -> Unit)
}Sibling files in the same folder:
- **`NostrSignerInternal.kt`** — in-process signer with the user's seckey in memory. Fastest; used for locally-stored accounts.
- **`NostrSignerSync.kt`** — blocking wrapper for scripts / migrations / tests where callbacks are inconvenient.
- **`EventTemplate.kt`** — the unsigned holder passed to `sign()`.
- **`SignerExceptions.kt`** — the error taxonomy (user denied, timeout, unsupported method, etc.).
- **`caches/`** — request cache so duplicate sign/encrypt requests coalesce.
Concrete implementations
- **Local (in-process)**: `NostrSignerInternal` — direct `Secp256k1Instance.signSchnorr` + NIP-44 inline. Used by accounts created/imported into Amethyst.
- **Remote (NIP-46 bunker)**: `quartz/.../nip46RemoteSigner/signer/NostrSignerRemote.kt`. Talks to a bunker service over Nostr DMs using the `BunkerRequest*` / `BunkerResponse*` event taxonomy (`BunkerRequestConnect`, `BunkerRequestSign`, `BunkerRequestNip44Encrypt`, …).
- **Android external (NIP-55)**: `quartz/src/androidMain/.../nip55AndroidSigner/client/NostrSignerExternal.kt`. Uses Android intents + content provider to delegate to another app on the same device. Launcher: `ExternalSignerLogin.kt`, `IActivityLauncher.kt`. Install-check: `IsExternalSignerInstalled.kt`.
The `SignerResult` Contract
Signers return via callback (and internally track via `SignerResult` sealed types in `nip46RemoteSigner/signer/SignerResult.kt` and `nip55AndroidSigner/api/SignerResult.kt`). Result variants cover success, user-denied, timeout, remote-disconnected, unsupported. Feature code should:
1. Pass a callback that handles success. 2. Trust the cache/timeout behavior — don't roll your own retry. 3. Surface `SignerExceptions` to the user with actionable messaging (e.g. "Bunker disconnected — reconnect?").
Typical Flow (Feature Code)
// High-level: Account methods already do this internally.
val signer: NostrSigner = account.signer // whichever kind the user configured
val template = reactionEventTemplate(noteId, authorPubKey, "+")
signer.sign(template) { signed ->
account.sendToRelays(signed) // or similar pipeline
}Most feature code should go through `Account`'s mutation methods (`account.sendReaction`, `account.follow`) rather than touching the signer directly — the account layer handles signing + publishing + local state update atomically. Reach for the signer directly only when `Account` doesn't have a helper.
Choosing a Signer at Sign-Up
Entry points:
- **Existing private key** (`nsec`, 32-byte hex, file) → `NostrSignerInternal`.
- **Bunker URL** (`bunker://...`) → `NostrSignerRemote.fromBunkerUri(bunkerUri, localSigner, client)` in `nip46RemoteSigner/signer/NostrSignerRemote.kt` parses the URI and returns a `NostrSignerRemote`; then call its `suspend fun connect()` to perform the NIP-46 handshake.
- **Installed external signer app** (Amber, nos2x, etc. on Android) → `ExternalSignerLogin.launch(...)` opens the signer app; approval yields a `NostrSignerExternal`.
The UI hosts both flows via `amethyst/.../ui/screen/loggedOff/login/` — look there for `ExternalSignerButton.kt` and the bunker-URL paste screen.
Trade-offs
| Signer | Latency | Offline OK? | Security | UX | |--------|---------|-------------|----------|-----| | Internal | µs | Yes | Key in app memory | No confirmation prompts | | Remote (NIP-46) | 100ms–seconds | No (needs bunker reachable) | Key never touches Amethyst | Occasional approval prompts | | External (NIP-55) | 100–500ms | Yes | Key in separate app | Prompt on every sign by default (configurable) |
Gotchas
- **Callbacks may never fire.** External signers can be dismissed without result; remote signers can time out. Use `SignerExceptions` / timeout handling at every call site or rely on the `Account` layer's wrapping.
- **`nip04Encrypt` is legacy** for NIP-04 DMs. New DM code should use NIP-17 gift-wrap → `nip44Encrypt` path.
- **Don't cache signer output** beyond the `caches/` that quartz already maintains. Stale cache entries lead to duplicate publishes.
- **Remote signer disconnects** need explicit reconnection UX — `RemoteSignerManager` exposes state; hook int
Other skills on amethyst.
- /account-state
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, each exposing a `.flow` StateFlow), `LocalCache` (the object-level event store backed by
Open skill - /amy-expert
Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/`
Open skill - /android-expert
Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4)
Open skill - /compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector
Open skill - /compose-modifier-and-layout-style
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout wrappers around a single conditional. Technique-layer skill — complements the codebase-specific compose-expert.
Open skill - /compose-recomposition-performance
Use when investigating Jetpack Compose recomposition performance, skippable/restartable composables, composables.txt or compiler reports, Layout Inspector recomposition counts, or frame-rate State reads in composition vs layout/draw, and it is not yet clear whether the cause is
Open skill

