5.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & development commands
./gradlew assembleDebug # debug build
./gradlew assembleRelease # release build (requires signing keystore)
./gradlew ktlintCheck # Kotlin style check (also runs on every git commit via pre-commit hook)
./gradlew ktlintFormat # auto-fix Kotlin style issues
./gradlew lint # Android Lint, warnings fail the build (runs on commit and push via hooks)
./gradlew test --no-daemon # unit tests (also runs on every git push via pre-push hook)
./build.sh # builds both offline and free release variants to ~/release/
Git hooks are auto-installed via the root build.gradle.kts preBuild task — no manual setup needed.
Build flavors
| Flavor | Purpose |
|---|---|
free (default) |
Online variant with full networking (OkHttp, Coil, relay connectivity) |
offline |
No network stack; use BuildFlavorChecker.isOfflineFlavor() to guard network code |
Architecture
Request ingestion — three paths
External apps and relays reach the signer through three distinct paths:
nostrsigner://Intent →SignerActivity→ parsed byIntentUtils→ shown as bottom sheet- ContentProvider IPC →
SignerProvider→ synchronous signing viarunBlocking - NIP-46 relay events (kind 24133) →
NotificationSubscription→EventNotificationConsumer→BunkerRequestUtils
All three paths converge on Account.sign() / encrypt/decrypt methods backed by NostrSignerInternal.
Global state — Amber.kt
Amber is the Application class and acts as the DI container. Key singletons it owns:
applicationIOScope—CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler), used for all background workclient: NostrClient— the Quartz Nostr relay clientnotificationSubscription— keeps the NIP-46 filter alive in the backgroundprofileSubscription— per-account, throttled one-shot metadata fetch; first fetches the user's NIP-65 relay list (kind 10002) and saves it locally, then fetches the metadata (kind 0) from the default profile relays plus the saved user relays; started/stopped by the composables that display each account viaProfileSubscriptionEffect(not app-wide)isStartingAppState: MutableStateFlow<Boolean>— set totrueduringrunMigrations(); code that must wait for startup callsisStartingAppState.first { !it }settings.killSwitch— when true, all relays are disconnected; checked before every relay operation
Per-account isolation
Every npub gets its own:
SharedPreferencesfile (prefs_${npub})AppDatabase(amber_db_${npub}) — apps + permissionsLogDatabase— operation logsHistoryDatabase— request history
All databases are lazy-loaded and cached in ConcurrentHashMaps in Amber. Account data (including decrypted keys) is loaded via LocalPreferences.loadFromEncryptedStorage() and cached in LargeCache.
Permission system
Permissions are stored in ApplicationEntity + ApplicationPermissionsEntity (Room). Each permission entry has:
rememberType— auto-accept, auto-reject, or always-askacceptUntil/rejectUntil— time-bound grantskind— event-kind-specific rules
Before showing the approval UI, all three ingestion paths query the database; if a matching auto-accept rule exists, signing proceeds silently.
Biometric / PIN lock is UI-only
The biometric/PIN prompt (useAuth / usePin in AmberSettings, configured in SecurityScreen) is only an app-launch UI gate, not a signing gate. It is rendered by BiometricAuthScreen, which is invoked exclusively from the two UI entry points — MainActivity and SignerActivity — to unlock the app's screens before any approval bottom sheet is shown.
It does not protect the signing operations themselves:
- ContentProvider IPC (
SignerProvider) and NIP-46 relay events (EventNotificationConsumer→BunkerRequestUtils) never touchBiometricAuthScreen,Biometrics,useAuth, orusePin. They sign in the background based purely on the permission database. - When an auto-accept rule matches, all three paths sign silently without triggering the biometric/PIN prompt — including
nostrsigner://intents, which auto-finish before the UI is interacted with.
In other words, the lock controls who can open and navigate the app UI; it does not stand between a request and Account.sign(). Authorization for automatic signing is governed solely by the permission system (rememberType / acceptUntil / kind).
Key files
| File | Purpose |
|---|---|
Amber.kt |
Application singleton, Nostr client, relay connectivity |
LocalPreferences.kt |
Account/settings persistence, encrypted key storage |
IntentUtils.kt |
Parses nostrsigner:// URIs, creates IntentData objects |
SignerProvider.kt |
ContentProvider IPC signing |
BunkerRequestUtils.kt |
NIP-46 protocol handling, relay responses |
AccountStateViewModel.kt |
Auth state, account switching, toast notifications |
ConnectivityService.kt |
Foreground service, network monitoring, relay reconnection |
BiometricAuthScreen.kt |
UI-only app-launch lock (biometric/PIN); not a signing gate |
Biometrics.kt |
Wraps BiometricPrompt / keyguard credential prompt |
SecurityScreen.kt |
Toggles useAuth / usePin and the re-prompt interval |
- For repository collaboration, use ngit and follow
.claude/skills/ngit/SKILL.md.