Imported from mplorentz/horcrux (
AGENTS.md). Install upstream withnpx skills add mplorentz/horcrux. Copyright stays with the author.
AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Horcrux is a Flutter app for backup and recovery of sensitive data using Shamir's Secret Sharing. Instead of cloud backups, data is distributed in encrypted shards to friends and family via the Nostr protocol. Recovery requires consent from multiple stewards to reassemble the data.
Key Technologies:
- Flutter with Dart SDK ^3.5.3 (tested on 3.41.9 in CI via Docker — see
.cursor/Dockerfile) - Nostr protocol via
ndkpackage for decentralized communication - Riverpod for state management and dependency injection
- Shamir's Secret Sharing via
ntcdcryptopackage - Flutter Secure Storage for key management
Coding Principles
The most expensive part of coding is having humans read and reason about code. Optimize for this by writing clear, simple, consise code. We want to handle all edge cases but avoid over-engineering, premature optimization, and defensive programming. Try to keep the length of files between 100 and 300 lines and keep the number of components down when possible. It's ok to repeat small amounts code once or twice if it avoids creating more components or complexity, but beyond that refactor into a shared component.
Delivering a clear, consistent developer experience when writing code is largely defined by the names and idioms that appear in APIs. Inclue all words needed to avoid ambiguity for a person reading the code. Omit needless words. Name variables parameters, and associated types according to their roles rather than their type constraints. Compensate for weak type information to clarify a parameter's role. Prefer method and function names that make use sites form grammatical English phrases. Begin names of factory methods with “make”, e.g. x.makeIterator().
Use imperative verbs for functions with side effects (i.e. processNewData()) and nouns for pure functions (i.e. newDataFromJSON()). Always use named parameters when the language supports it and always document functions with a short summary of their implementation and any unexpected behavior.
Name Mutating/nonmutating method pairs consistently (i.e. x.sort() vs. x.sorted()). A mutating method will often have a nonmutating variant with similar semantics, but that returns a new value rather than updating an instance in-place. Uses of Boolean methods and properties should read as assertions about the receiver when the use is nonmutating, e.g. x.isEmpty, line1.intersects(line2).
Protocols that describe what something is should read as nouns (e.g. Collection).
Protocols that describe a capability should be named using the suffixes able, ible, or ing (e.g. Equatable, ProgressReporting).
The names of other types, properties, variables, and constants should read as nouns.
Avoid obscure terms if a more common word conveys meaning just as well. Don’t say “epidermis” if “skin” will serve your purpose. Terms of art are an essential communication tool, but should only be used to capture crucial meaning that would otherwise be lost.
Avoid abbreviations. Abbreviations, especially non-standard ones, are effectively terms-of-art, because understanding depends on correctly translating them into their non-abbreviated forms. The intended meaning for any abbreviation you use should be easily found by a web search.
Embrace precedent. Don’t optimize terms for the total beginner at the expense of conformance to existing culture.
Avoid negatives in variable names. Instead of suppressLocalNotification = false prefer showLocalNotification = true.
Common Development Commands
Note: This project uses fvm (Flutter Version Manager). All flutter commands must be prefixed with fvm.
Running the App
# Run on default device
fvm flutter run
# Run on specific device
fvm flutter run -d chrome # Web
fvm flutter run -d macos # macOS
fvm flutter run -d ios # iOS simulator
# Run with specific flavor
fvm flutter run --debug
fvm flutter run --release
Testing
# Run all unit tests (excluding golden tests)
fvm flutter test --exclude-tags=golden
# Run golden screenshot tests
fvm flutter test --tags=golden
# Run tests in a specific file
fvm flutter test test/services/backup_service_test.dart
# Update golden test images (must be run on macOS)
fvm flutter test test/screens --update-goldens
# Run with coverage
fvm flutter test --coverage
Code Quality
# Format code
fvm dart format .
# Check formatting without modifying files
fvm dart format --set-exit-if-changed .
# Run static analysis
fvm flutter analyze
# Get dependencies
fvm flutter pub get
# Clean build artifacts
fvm flutter clean
Building
# Build for web (HTML renderer for testing)
fvm flutter build web --web-renderer html
# Build for macOS
fvm flutter build macos
# Build for iOS
fvm flutter build ios
Web Testing with Playwright
# Build and serve web app for testing (see .vscode/tasks.json)
fvm flutter build web --web-renderer html
cd build/web
python3 -m http.server 8084
Architecture
State Management: Riverpod Pattern
All state management uses Riverpod with a strict service-based architecture:
Services contain business logic and are always instance classes with dependency injection:
final myServiceProvider = Provider<MyService>((ref) {
return MyService(
ref.read(repositoryProvider),
ref.read(otherServiceProvider),
);
});
class MyService {
final MyRepository _repository;
MyService(this._repository);
Future<Result> doBusinessLogic() async { ... }
}
Repositories are only created when data access is complex (caching, streams, multiple queries, 100+ lines). See .cursorrules for detailed guidance. Simple CRUD operations stay in services directly.
Providers expose data to UI components using various types:
Provider<T>- For services and singletonsFutureProvider<T>- For async data loadingStreamProvider<T>- For reactive streamsStreamProvider.family<T, Param>- For parameterized streams
Core Services
LoginService (lib/services/login_service.dart): Manages Nostr key pairs via Flutter Secure Storage. Keys are generated on first launch or during onboarding.
NdkService (lib/services/ndk_service.dart): Core Nostr protocol integration. Manages NDK connections, subscriptions, and gift-wrapped event handling. Provides streams for recovery requests and responses.
VaultRepository (lib/providers/vault_provider.dart): Repository pattern for vault CRUD. Manages in-memory cache and SharedPreferences persistence. Emits streams for reactive UI updates.
BackupService (lib/services/backup_service.dart): Orchestrates the backup process: secret sharing, encryption, and distribution to stewards via Nostr.
RecoveryService (lib/services/recovery_service.dart): Handles recovery workflow: sending requests to stewards, collecting shard responses, and reassembling secrets.
InvitationService (lib/services/invitation_service.dart): Manages invitation links and acceptance flow for adding stewards to vaults.
RelayScanService (lib/services/relay_scan_service.dart): Background service that continuously scans Nostr relays for incoming events (gift-wrapped messages, shard confirmations, recovery responses).
App Initialization Flow
main.dartwraps app inProviderScopefor Riverpod_initializeApp()checks for existing Nostr key viaLoginService- If key exists, calls
initializeAppServices()to start:- Deep link handling (
DeepLinkService) - Relay scanning (
RelayScanService)
- Deep link handling (
- If no key, shows
OnboardingScreento generate one - After onboarding, invalidates key providers to trigger app rebuild
Nostr Protocol Integration
Event Types (defined in lib/models/nostr_kinds.dart):
- Gift-wrapped events (NIP-44) for encrypted peer-to-peer messaging
- Custom kinds for shard distribution, confirmations, and recovery
Key Format Conventions:
- Internal: Hex format (64 chars, no prefix) for storage, processing, API payloads
- Display: Bech32 format (
npub1...,nsec1...) for UI, user input, logs
Event Payloads: Always use snake_case (not camelCase) in raw Nostr event JSON. NDK automatically converts to camelCase when processing.
UI Architecture
Theme System: Uses horcrux3 theme (lib/widgets/theme.dart) with muted, professional palette.
CRITICAL DESIGN RULE: Orange (#DC714E) appears ONLY on RowButton components (primary actions at bottom of screen). All other UI uses Navy-Ink or Umber.
Key Widgets:
RowButton- Single primary action (orange, full-width, bottom)RowButtonStack- Multiple actions with gradient (orange at bottom)VaultCard- List item for vaultsKeyHolderList- Display stewards with status
Always reference DESIGN_GUIDE.md before making UI changes. It contains the complete color palette, typography system, and component patterns.
Data Models
Vault (lib/models/vault.dart): Core data model. Contains encrypted content, metadata, shards, recovery requests, and backup config. Has VaultState enum (recovery/owned/keyHolder/awaitingKey) based on current user's relationship.
ShardData: Represents a single shard of the secret. Includes shard index, encrypted data, and steward pubkey.
BackupConfig: Shamir parameters (threshold, totalKeys) and steward list with statuses.
RecoveryRequest: Tracks active recovery attempts with request ID, status, and collected shards.
KeyHolder: Represents a person holding a shard, with status tracking (pending/confirmed/failed).
Breaking Circular Dependencies
When services depend on each other, add explicit types to providers:
final Provider<ServiceA> serviceAProvider = Provider<ServiceA>((ref) {
final ServiceB serviceB = ref.read(serviceBProvider);
return ServiceA(serviceB);
});
final Provider<ServiceB> serviceBProvider = Provider<ServiceB>((ref) {
final ServiceA serviceA = ref.read(serviceAProvider);
return ServiceB(serviceA);
});
Testing Guidelines
Golden Tests
- Screenshot tests use
golden_toolkitpackage - MUST be run on macOS for consistent rendering (CI enforces this)
- Test config in
test/flutter_test_config.dartloads bundled fonts - Update goldens:
flutter test test/screens --update-goldens - Golden files stored in
test/screens/goldens/
Unit Tests
- Mock dependencies using
mockitopackage - Run mocks generator:
flutter pub run build_runner build - Test files mirror lib structure:
test/services/,test/models/, etc.
CI/CD
- GitHub Actions workflow in
.github/workflows/test.yml - Runs on macOS for golden test consistency
- Steps: format check → analyze → unit tests → golden tests
- Failed goldens upload artifacts with before/after images
Security Considerations
Key Storage: Nostr private keys stored in Flutter Secure Storage (platform keychain). Never log or expose private keys.
Encryption: Uses NIP-44 gift-wrapping for peer-to-peer Nostr events. All shard distribution and recovery messages are encrypted.
Shamir Constraints: Min threshold 1, max total keys 10 (see VaultBackupConstraints).
Deep Links: Invitation links are generated (InvitationLink.toUrl()) as https://horcruxbackup.com/invite/{inviteCode}?vault=...&owner=...&relays=...; the horcrux:// custom scheme is also accepted for the same /invite/{code} path. Handle via DeepLinkService with validation.
Important Cursor Rules (from .cursorrules)
Service/Repository Pattern: Only create repositories for complex data access (caching, streams, 100+ lines). Use service-only pattern for simple CRUD.
No Thin Wrappers: Don't create repositories that just delegate to services. Use service directly with providers.
Nostr Conventions:
- Hex format for internal data, bech32 for display
- Snake_case in Nostr event payloads
Design System: Orange only on RowButton. Check DESIGN_GUIDE.md before UI work.
Debugging
Logging: Use Log class from lib/services/logger.dart:
Log.info('Message');
Log.error('Error occurred', exception);
Log.debug('Debugging info');
Debug Sheet: DebugInfoSheet widget shows current pubkey, relay status, and app state.
Relay Issues: Check RelayScanService logs for connection failures. Default relays in RelayConfiguration.
Project Status
Alpha Software: Not production-ready. Do not use for real secrets.
Funding: OpenSats.org (Bitcoin/Nostr open-source development)
License: MIT
Tester Playbook: Flutter + Marionette
This playbook describes how to run and interact with the Horcrux Flutter app
for behavioral testing. The tester agent reads this section from the repo's
AGENTS.md to know which tools to use and how to set up the environment.
Testing Environment
Use Docker, not native Xvfb. Each container gets its own D-Bus session + gnome-keyring, so Nostr keypairs don't collide between instances. Native Xvfb shares the keyring across instances — unworkable for multi-instance testing (invitations, shard distribution, recovery).
Dockerfile: .cursor/Dockerfile — Flutter 3.41.9, gcc-12, all required
system libraries (libssl-dev, libsqlite3-dev, etc.) pre-installed.
Scripts
| Script | Purpose |
|---|---|
scripts/run-app-in-docker.sh |
Build, start, and run the app in a single Docker container with live log streaming. VNC on port 5900 (password: horcrux). |
scripts/get-vm-uri.sh |
Extract the Marionette VM Service WebSocket URI from the running container. |
scripts/hot-reload-docker.sh |
Trigger hot reload via SIGUSR1 to the Flutter process in the container. |
scripts/run-two-instances.sh |
Launch two containers (owner + steward) for end-to-end backup/recovery testing. |
scripts/setup-x11.sh |
Source-able script: starts Xvfb, D-Bus, gnome-keyring, and notification-daemon inside a container. |
Quick Start (Single Instance)
# Build and run the app in Docker (Press Ctrl+C to stop)
./scripts/run-app-in-docker.sh
# In another terminal, get the Marionette WebSocket URI
./scripts/get-vm-uri.sh
# → ws://localhost:8182/XXXXXXXXXXXXXX/ws
# Connect via Marionette MCP and start testing
Quick Start (Two Instances: Owner + Steward)
# Launch both containers
./scripts/run-two-instances.sh
# Get VM service URIs (runs in background, ~2-3 min to start)
docker exec horcrux-owner grep 'ws://' /tmp/flutter_run.log
docker exec horcrux-steward grep 'ws://' /tmp/flutter_run.log
# Connect Marionette to each URI to interact with each app
# (connect/disconnect between instances to switch)
# Cleanup
docker compose -f .cursor/docker-compose.two-instance.yml down -v
Marionette MCP Interaction
Marionette MCP (marionette_mcp) is available as a Dart global activation
(~/.pub-cache/bin/marionette_mcp). It connects to the Flutter app via the
Dart VM service WebSocket.
Connect:
marionette_connect(uri: "ws://localhost:8182/TOKEN/ws")
Available RPCs:
interactiveElements— list all tappable/enterable elements with boundstap(key: ..., type: ..., x: ..., y: ...)— tap by key, type, or coordinatesenterText(key: ..., type: ..., input: ...)— enter text (param isinput, nottext)scrollTo— scroll to an elementgetLogs— retrieve app logstakeScreenshots— capture screenshots
Key quirks:
- Tap-by-key does not match visible text — it matches widget
Keyproperties - Most Horcrux buttons don't have keys; use x/y coordinates from
interactiveElementsbounds instead - Fields with keys:
vault_name_field,owner_name_field,vault_content_field,self_steward_switch,alert_stewards_push_switch— these work with key-based operations enterTextusesinput(nottext) as the parameter name- The
--no-ddsflag and socat proxy (port 8182) avoid the DDS proxy layer that breaks extension RPC calls
Container Reference
- Container name:
horcrux-cursor-agent(single instance) - Container names:
horcrux-owner,horcrux-steward(two instances) - VNC: port 5900, password
horcrux - Marionette proxy: port 8182 (socat forwards to VM service port 8181)
- Logs:
docker exec horcrux-cursor-agent tail -f /tmp/flutter_run.log - Hot reload:
./scripts/hot-reload-docker.sh
Common Pitfalls
| Pitfall | Explanation |
|---|---|
| Bridge networking broken | Must use --network=host on this host. Docker build also needs --network=host. |
| Deep links don't work in Docker | Use the 'npub' advanced option to add stewards instead of invitation links. |
| Push notifications fail | Expected in Docker (no push service). 403 errors are harmless. |
| gnome-keyring must be unlocked | setup-x11.sh handles this. If the app crashes at startup, check keyring. |
| gcc-12, not gcc-13 | linux/CMakeLists.txt pins gcc-12/g++-12. The system may have gcc-13 installed but it won't work. |
| libssl-dev required | Without it, sqlcipher_flutter_libs CMake fails with 'Could NOT find OpenSSL'. |
| libsqlite3-dev required | Without it, drift in-memory tests fail (libsqlite3.so not found). |
| Golden tests skip on Linux | Golden screenshot tests are macOS-only. Always use --exclude-tags=golden. |
Cursor Cloud specific instructions
Environment overview
Flutter is installed at /opt/flutter and available on PATH alongside $HOME/.pub-cache/bin. Node.js 20 is installed for Nostrbook MCP. All Linux desktop system dependencies (GTK3, libsecret, gnome-keyring, Xvfb, x11vnc, etc.) are pre-installed. The project does not use fvm in the cloud environment — use flutter and dart directly (no fvm prefix).
Running the app natively (not Docker)
The Linux app requires a virtual display, D-Bus session, and gnome-keyring before launch:
# Start Xvfb (if not already running)
Xvfb :99 -screen 0 600x1024x24 &
export DISPLAY=:99
# D-Bus + gnome-keyring (required by flutter_secure_storage)
eval "$(dbus-launch --sh-syntax)"
mkdir -p ~/.cache ~/.local/share/keyrings
printf '\n' | gnome-keyring-daemon --unlock 2>/dev/null || true
gnome-keyring-daemon --start --components=secrets --daemonize 2>/dev/null || true
# Optional: notification daemon for flutter_local_notifications
notification-daemon &
# Run the app
cd /workspace
flutter run -d linux --debug --host-vmservice-port=8181
The Dart VM Service URI appears in the output (e.g. http://127.0.0.1:8181/<token>/). The WebSocket URI for Marionette MCP is ws://127.0.0.1:8181/<token>/ws.
Key gotchas
- gcc-12 is required:
linux/CMakeLists.txtpinsgcc-12/g++-12. The system also has gcc-13 but the Flutter Linux build will fail without gcc-12/g++-12 specifically installed. - libssl-dev is required: The
sqlcipher_flutter_libspackage needs OpenSSL development headers (libssl-dev) to compile. Without it, the CMake build fails with "Could NOT find OpenSSL". - libsqlite3-dev is required for tests: The drift database unit tests (
test/database/app_database_test.dart) needlibsqlite3.soto run in-memory SQLite. Installlibsqlite3-dev. - Golden tests skip on Linux: Golden screenshot tests are macOS-only (rendering differs). Always use
--exclude-tags=goldenwhen running tests on Linux:flutter test --exclude-tags=golden - gnome-keyring must be unlocked before the app starts, otherwise
flutter_secure_storagewill crash. The empty-password unlock shown above is sufficient for dev/test. - Hot reload: Send
SIGUSR1to the Flutter process, or typerin theflutter runterminal.
Beads (bd) issue tracker
bd (beads) v1.0.3 is installed at /usr/local/bin/bd. The project's issue database lives on a remote Dolt SQL server (dolt.lorentz.is:3307, database horcrux_app). To connect:
- The
BEADS_DOLT_PASSWORDsecret must be set (Cursor secret for usercursor_cloud) - After
bd initin server mode,bdconnects to the remote Dolt server directly - Agents: Routine commands (
bd create,bd update,bd close, notes, etc.) write to that server. Do not treatbd dolt pushas a required end-of-session step here unless a maintainer asks for it (for example local or offline Dolt workflows). - Run
bd primeat session start to load workflow context - Init command (run once per fresh VM):
BEADS_DOLT_PASSWORD="$BEADS_DOLT_PASSWORD" bd init --non-interactive --server --server-host=dolt.lorentz.is --server-port=3307 --server-user=cursor_cloud --external --database=horcrux_app --prefix=horcrux_app --skip-agents --skip-hooks
MCP servers
- Marionette MCP:
marionette_mcp(Dart global activation, on PATH via~/.pub-cache/bin). Configured in.cursor/mcp.json. - Nostrbook MCP:
npx -y @nostrbook/mcp@latest. Configured in.cursor/mcp.json.
QA Testing (Linux, multi-instance)
Use Docker, not native Xvfb. Each container gets its own D-Bus session + gnome-keyring, so Nostr keypairs don't collide between instances. Native Xvfb shares the keyring across instances — unworkable for multi-instance testing (invitations, shard distribution, recovery).
Docker build & run:
docker build --network=host -f .cursor/Dockerfile -t horcrux-dev . # Bridge networking is broken on this host; must use --network=host
docker run -d --name horcrux-dev --network=host -v /home/gascity/horcrux:/workspace horcrux-dev tail -f /dev/null
Inside container, launch the app:
Xvfb :99 -screen 0 600x1024x24 &
export DISPLAY=:99
eval "$(dbus-launch --sh-syntax)"
mkdir -p ~/.cache ~/.local/share/keyrings
printf '\n' | gnome-keyring-daemon --unlock 2>/dev/null || true
gnome-keyring-daemon --start --components=secrets --daemonize 2>/dev/null || true
flutter run -d linux --debug --host-vmservice-port=8181 --no-dds
Marionette via VM service WebSocket: Since marionette_mcp is not in pi's MCP server config, interact with the app by calling Marionette extension RPCs directly through the Dart VM service WebSocket (ws://127.0.0.1:PORT/TOKEN/ws). Available RPCs: ext.flutter.marionette.interactiveElements, ext.flutter.marionette.tap (params: key, type, x, y), ext.flutter.marionette.enterText (params: key, type, input — not 'text'), ext.flutter.marionette.scrollTo, ext.flutter.marionette.getLogs, ext.flutter.marionette.takeScreenshots. Tap-by-key does NOT match visible text — use x/y coordinates from interactiveElements bounds instead. Fields with keys (vault_name_field, owner_name_field, vault_content_field, self_steward_switch) work with key-based operations. See bead horcrux_app-f1h7 for adding marionette_mcp to pi's config.
Multi-instance testing: Use docker-compose with separate containers per app instance (each on its own VM service port).
Standard commands reference
See AGENTS.md sections above for lint (flutter analyze), test (flutter test --exclude-tags=golden), build (flutter build linux --debug), and format (dart format .) commands. Omit the fvm prefix in this environment.
Beads Issue Tracker
This project uses bd (beads) for issue tracking. Run bd prime to see full workflow context and commands.
Quick Reference
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
Rules
- Use
bdfor ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run
bd primefor detailed command reference and session close protocol - Use
bd rememberfor persistent knowledge — do NOT use MEMORY.md files - When you open a pull request for work tracked in bd, include the bead issue ID in the PR description (for example
horcrux_app-tco). Usebd show <id>to copy the exact ID string.
Task tracking in Cursor
- Upstream beads rule (auto-generated; re-run
bd setup cursorto refresh): .cursor/rules/beads.mdc - Horcrux stage workflows (feature vs bug, writeback rules): .cursor/rules/beads-workflow.mdc
- Label glossary: .beads/STAGES.md
Session Completion
When ending a work session, you MUST complete ALL steps below. Work is NOT complete until git push succeeds.
MANDATORY WORKFLOW:
-
File issues for remaining work - Create issues for anything that needs follow-up
-
Run quality gates (if code changed) - Tests, linters, builds
-
Update issue status - Close finished work, update in-progress items
-
PUSH TO REMOTE - This is MANDATORY:
git pull --rebase git push git status # MUST show "up to date with origin"Beads: With the configured remote Dolt server, updating issues via normal
bdcommands is sufficient; agents do not need to runbd dolt push. -
Clean up - Clear stashes, prune remote branches
-
Verify - All changes committed AND pushed
-
Hand off - Provide context for next session
CRITICAL RULES:
- Work is NOT complete until
git pushsucceeds - NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
Coder Quality Gates (run before committing/pushing)
The coder agent MUST run these before handing off a bead. They mirror what
CI enforces (see .github/workflows/test.yml). Run them in order:
# 1. Regenerate code (mocks, drift, etc.) — CI fails if generated files are stale
flutter pub run build_runner build --delete-conflicting-outputs
dart format .
# 2. Lint
flutter analyze
# 3. Unit tests (excludes golden + drift-schema which run separately)
flutter test --exclude-tags=golden,drift-schema
# 4. Drift schema parity (re-dump and diff against committed drift_schemas/)
flutter test test/database/schema_parity_test.dart
# NOTE: if you changed the schema, bump schemaVersion in
# lib/database/app_database.dart then re-dump:
# dart run drift_dev schema dump lib/database/app_database.dart drift_schemas/
# (CI enforces version bump via scripts/check-drift-schema-version-bump.sh)
# 5. Golden tests — ONLY run if you changed UI; requires a display/rasterizer
flutter test --tags=golden
# If goldens fail because the UI intentionally changed:
# flutter test test/screens --update-goldens (then commit the PNGs)
# Golden reference images can only be regenerated on macOS/CI (no rasterizer
# on Linux). If you changed UI on Linux, state in your handoff comment that
# goldens need regeneration on CI/macOS.
Rules:
- Run ALL gates before handoff, not just the ones that pass.
- If a gate fails, fix it before pushing. Never push red CI.
- If a gate genuinely can't run in your environment (e.g. goldens need macOS), say so explicitly in the bead handoff comment — do NOT silently skip it.
- CI also checks the drift schema version bump on schema changes — if you
touched
lib/database/, runscripts/check-drift-schema-version-bump.sh mainor manually verify the version file.
