This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Igloo is a desktop key management and remote signing application for the FROSTR protocol, which implements k-of-n threshold signatures using FROST (Flexible Round-Optimized Schnorr Threshold signatures) for nostr. Built with Electron, React, and TypeScript.
Architecture: Igloo uses a hybrid architecture where the desktop app handles UI/UX and file management while @frostr/igloo-core (external library) handles all cryptographic operations, validation, and node management.
npm install # Install dependencies
npm run dev # Start webpack watch + Electron with hot reload
npm start # Build once, then launch Electron
npm run build # Compile TypeScript and bundle to dist/
npm run watch # TypeScript watch mode onlynpm test # Run full Jest test suite
npm run test:watch # Jest watch mode for development
npm test -- <file> # Run specific test file
npm test -- --coverage # Generate coverage reportnpm run lint # Auto-fix linting issues
npm run lint:check # Check without fixingnpm run dist # Build for current platform
npm run dist:mac # macOS signed & notarized build
npm run dist:mac-unsigned # macOS unsigned (dev only)
npm run dist:win # Windows build
npm run dist:linux # Linux AppImage and .deb./scripts/release.sh X.Y.Z # Create GPG-signed git tag with version bump
git push origin main && git push origin vX.Y.Z # Trigger CI release workflowShares: FROST protocol splits a nostr nsec (private key) into multiple shares using Shamir Secret Sharing. Individual shares are useless; threshold number of shares can reconstruct the key or create valid signatures.
Keysets: A collection of shares that work together, identified by a unique group key. Shares from different keysets cannot be mixed.
Remote Signing: Signing nodes communicate over nostr relays with end-to-end encryption to coordinate threshold signatures without reconstructing the full key.
src/
├── main.ts # Electron main process, IPC handlers
├── renderer.tsx # React app entry point
├── components/
│ ├── App.tsx # Main routing and app state
│ ├── Create.tsx # Generate or import keyset
│ ├── Keyset.tsx # Display shares, QR codes
│ ├── SaveShare.tsx # Password-encrypt and save shares
│ ├── ShareList.tsx # Detect and list saved shares
│ ├── LoadShare.tsx # Decrypt and load share into memory
│ ├── AddShare.tsx # Import existing share from another device
│ ├── Signer.tsx # Start signing node, handle requests
│ ├── EventLog.tsx # Display signing events
│ ├── Recover.tsx # Reconstruct nsec from threshold shares
│ └── ui/ # shadcn/ui components
├── lib/
│ ├── shareManager.ts # File system share operations (main process)
│ ├── clientShareManager.ts # IPC wrapper for renderer (calls shareManager)
│ ├── encryption.ts # PBKDF2 password derivation for share files
│ ├── filesystem.ts # Electron app data path utilities
│ ├── validation.ts # Input validation helpers
│ ├── echoRelays.ts # Relay planning and normalization utilities
│ ├── signer-keepalive.ts # Connection persistence and auto-reconnect
│ └── utils.ts # General utilities, cn() for Tailwind
├── types/
│ └── index.ts # Shared TypeScript types
└── __tests__/
├── setup.ts # Jest config, global mocks
├── __mocks__/ # Shared mock implementations
├── integration/ # Electron IPC, file system tests
├── workflows/ # End-to-end user flow tests
├── components/ # React component tests
└── lib/ # Library unit tests
Main Process (src/main.ts):
- Creates BrowserWindow
- Registers IPC handlers for share operations
- Instantiates
ShareManagerfor file system access - Shares stored in
<appData>/igloo/shares/*.json
Renderer Process (src/renderer.tsx, components):
- React UI with no direct file system access
- Calls
ClientShareManagerwhich usesipcRenderer.invoke() - All cryptographic operations delegate to
@frostr/igloo-core
get-shares: List all saved sharessave-share: Encrypt and write share to file systemdelete-share: Remove share fileopen-share-location: Open Finder/Explorer at share location
App-level state in App.tsx tracks:
showingCreate,showingRecover,showingAddShare: Boolean flags for screen routingkeysetData: Temporary in-memory keyset (cleared after saving shares)signerData: Currently decrypted share and metadata for signinghasShares: Whether any shares exist in file systemactiveTab: Current tab selection (signer/recovery)
Components communicate via props and callbacks; no Redux or external state library. The Signer component exposes a ref handle (SignerHandle) to allow App.tsx to stop the signer when navigating away.
All crypto operations are handled by the external library:
generateKeysetWithSecret(): Create keyset from nsecvalidateShare(),validateGroup(): Input validationdecodeShare(),decodeGroup(): Parse encoded credentialscreateConnectedNode(): Initialize signing noderecoverSecretKeyFromCredentials(): Reconstruct nsec from threshold sharescleanupBifrostNode(): Properly disconnect signing node
Do not implement cryptographic logic in the desktop app—always use @frostr/igloo-core.
Shares are stored as encrypted JSON in <appData>/igloo/shares/:
{
"id": "unique-id",
"name": "Share 1",
"share": "encrypted-share-data",
"salt": "hex-salt",
"groupCredential": "group-key",
"savedAt": "ISO-timestamp",
"metadata": {
"binder_sn": "8-char-prefix"
}
}Encryption: PBKDF2 with 600,000 iterations (v1 shares), user-provided password, random salt. Legacy shares (no version field) use 32 iterations.
Relays are configured via <appData>/igloo/relays.json:
{
"relays": ["wss://relay1.example.com", "wss://relay2.example.com"]
}The computeRelayPlan() function in echoRelays.ts merges relays from multiple sources using conditional logic with two distinct paths:
When envRelay is present:
- Priority: explicit relays >
envRelay> group relays
When envRelay is absent:
- Priority: explicit relays > configured relays (
relays.json) →baseRelaysparameter →DEFAULT_ECHO_RELAYSas fallbacks > group relays - The
defaultschain resolves as: ifrelays.jsonexists and contains relays, use those; otherwise fall back to thebaseRelaysparameter; if that's also absent, useDEFAULT_ECHO_RELAYSfrom@frostr/igloo-core - Group relays are appended after the defaults chain
The function references: envRelay (from IGLOO_RELAY environment variable or passed parameter), computeRelayPlan, baseRelays, DEFAULT_ECHO_RELAYS, and relays.json (read via readConfiguredRelaysSync()).
Tests focus on desktop-specific functionality:
- Electron IPC communication
- File system operations
- React component behavior
- User workflows across components
- Desktop features (clipboard, QR codes, file explorer)
Core cryptographic logic is tested in @frostr/igloo-core, not here.
__tests__/integration/: IPC and file system__tests__/workflows/: End-to-end user flows__tests__/components/: React component tests__tests__/lib/: Utility unit tests__tests__/__mocks__/: Shared mocks for external dependencies
- Mock
@frostr/igloo-corefunctions at the module level (seesetup.ts) - Mock Electron's
ipcRendererandappAPIs - Test both success and error paths
- Use
jest.clearAllMocks()inbeforeEach - Focus on behavior, not implementation details
- Strict mode enabled
- Use explicit types for function parameters and returns
- Prefer interfaces over types for object shapes
- Use
@/path alias for imports:import { foo } from '@/lib/utils'
- Functional components with hooks
- PascalCase for component files:
SaveShare.tsx - camelCase for functions and variables
- kebab-case for non-component files under
components/ui/
- Components:
PascalCase(SaveShare, ClientShareManager) - Functions:
camelCase(saveShare, validateRelay) - Constants:
UPPER_SNAKE_CASE(DEBUG_GROUP_AUTO) - Files:
kebab-casefor utilities,PascalCasefor components
- Tailwind CSS utility classes in JSX
- Use
cn()utility from@/lib/utilsfor conditional classes - Avoid inline styles unless conditionally computed
- shadcn/ui components in
components/ui/
- ESLint with TypeScript, React, and React Hooks rules
- Run
npm run lintbefore committing - Address warnings; don't suppress unless necessary
- Production releases are code-signed with Apple Developer ID and notarized
- Requires environment variables:
CSC_LINK,CSC_KEY_PASSWORD,APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD,APPLE_TEAM_ID - See MAC_SIGNING_SETUP.md for detailed setup
- Development builds: use
npm run dist:mac-unsigned
All releases include:
- GPG signing: Git tags and release artifacts signed with developer PGP key
- macOS code signing: macOS apps signed with Apple Developer ID and notarized
./scripts/release.sh X.Y.Zupdatespackage.jsonand creates GPG-signed git tag- Push tag to trigger GitHub Actions CI
- CI builds for all platforms, signs artifacts, generates checksums
- Automated release creation with signed binaries
- Windows:
.exeinstaller and portable - macOS:
.dmgand.zipfor Intel (x64) and Apple Silicon (arm64) - Linux:
.AppImageand.deb
- Create
src/components/MyComponent.tsx - Import into
App.tsxand add to routing logic - Add IPC handlers in
main.tsif file system access needed - Add tests in
src/__tests__/components/MyComponent.test.tsx
- Register handler in
main.ts:ipcMain.handle('my-channel', async (_, args) => { ... }) - Add method to
ClientShareManageror create new client class - Use
ipcRenderer.invoke('my-channel', args)in renderer - Add integration test in
__tests__/integration/
Don't. Coordinate with @frostr/igloo-core maintainers to add features to the core library, then import and use them.
- Development mode opens DevTools automatically
- Use
console.login both main and renderer processes - Main process logs appear in terminal; renderer logs in DevTools
- Set
DEBUG_GROUP_AUTO = truein relevant files for verbose logging
electron: Desktop app frameworkreact+react-dom: UI library (v17)@frostr/igloo-core: Core cryptographic operations@frostr/bifrost: Nostr relay communication@noble/hashes,@noble/curves,@noble/ciphers: Crypto primitives@radix-ui/*: Accessible UI primitivestailwindcss: Utility-first CSSzod: Schema validation
typescript: Languagewebpack: Bundler for renderer processelectron-builder: Packaging and distributionjest+ts-jest: Testing framework@testing-library/react: React testing utilitieseslint: Linting
- Never commit sensitive keys, tokens, or passwords
- Shares are encrypted with user passwords; never store plaintext
- All signing communication uses end-to-end encryption via bifrost
- GPG keys and Apple Developer certificates stored in GitHub Secrets
- macOS: Requires proper code signing and notarization (no ad-hoc signing)
- Windows: NSIS installer or portable executable
- Linux: AppImage (universal) or .deb (Debian/Ubuntu)
Desktop app should remain thin—delegate crypto operations to @frostr/igloo-core. If you need to add validation, node management, or key operations, check if @frostr/igloo-core already provides it or should be extended.
- Electron's main process uses CommonJS (
require), renderer uses ESM (import) @frostr/igloo-coreis mocked in tests to avoid ESM/Jest complications- macOS notarization happens post-build via
scripts/notarize.cjs - Version must match between
package.jsonand git tag or CI will fail