Safety Numbers & Identity Management
This guide covers Signal’s identity/safety number system and the bot’s tools for managing user identities, detecting abandoned accounts, and maintaining community hygiene.
Background: Signal Safety Numbers
Section titled “Background: Signal Safety Numbers”What Are Safety Numbers?
Section titled “What Are Safety Numbers?”Signal uses end-to-end encryption where each user has a unique cryptographic identity. When you communicate with someone, Signal generates a “safety number” - a fingerprint of both parties’ identity keys that can be verified out-of-band to confirm you’re talking to the right person.
When Safety Numbers Change
Section titled “When Safety Numbers Change”A user’s safety number changes when they:
- Reinstall Signal on the same device
- Switch to a new device
- Re-register their phone number
- Delete and recreate their Signal account
The Trust Problem
Section titled “The Trust Problem”When a safety number changes, Signal shows a notification: “Safety number changed”. You have two options:
- Verify - Confirm with the person out-of-band and mark as verified
- Trust - Accept the new identity without verification
For community bots managing thousands of users, manual verification isn’t practical.
Automated Safety Number Detection (Identity Polling)
Section titled “Automated Safety Number Detection (Identity Polling)”How It Works
Section titled “How It Works”The bot proactively detects safety number changes by polling signal-cli’s identity store every 5 minutes. This catches changes that would otherwise be silently auto-trusted by signal-cli.
Every 5 minutes: 1. Call listIdentities via JSON-RPC (returns ~2300 identities) 2. Compare each fingerprint against stored value in database 3. If fingerprint changed AND user is in a community group: → Trigger safety number verification flow → Add user to Entry/INDOC → Create 24h verification request → Notify admins in Actions chatWhy This Is Needed
Section titled “Why This Is Needed”Signal-cli starts with --trust-new-identities always which silently auto-trusts new identity keys without throwing UntrustedIdentityException. This means the exception-based detection misses many changes. The polling approach closes this gap.
Two-Phase Initialization
Section titled “Two-Phase Initialization”On first startup (or after a restart), the bot:
- Seeds all current fingerprints into the database (no alerts)
- Subsequent polls only trigger on actual fingerprint changes
This prevents false positives where every identity appears “new” after a restart.
Database Tables
Section titled “Database Tables”| Table | Purpose |
|---|---|
known_identity_fingerprints | Stores UUID → fingerprint mapping for 2300+ identities |
identity_change_log | Audit trail of all detected fingerprint changes |
Monitoring
Section titled “Monitoring”Check recent identity changes:
-- Recent changesSELECT uuid, display_name, action_taken, detected_atFROM identity_change_logORDER BY detected_at DESC LIMIT 10;
-- Users with most changesSELECT uuid, display_name, change_count, fingerprint_changed_atFROM known_identity_fingerprintsWHERE change_count > 0ORDER BY change_count DESC;Look for [IDENTITY_POLL] in bot logs:
docker logs signal-bot-selfhosted-signal-bot-1 2>&1 | grep IDENTITY_POLLBot Identity Commands
Section titled “Bot Identity Commands”Trust Command (trust)
Section titled “Trust Command (trust)”The signal-cli trust command accepts a user’s current identity keys:
await sendRequest('trust', { recipient: 'uuid-here', trustAllKnownKeys: true});Important Limitation: The trust command succeeds even for unregistered accounts. It only verifies that identity keys exist in signal-cli’s local store, not that the account is currently active on Signal.
Detection Gap
Section titled “Detection Gap”Users who:
- Passed trust check (identity keys exist)
- But show “This person isn’t using Signal” in the app
These are abandoned accounts - the user deleted Signal or re-registered with a different number, but the old identity keys remain cached.
Detecting Abandoned Accounts
Section titled “Detecting Abandoned Accounts”The UNREGISTERED_FAILURE Method
Section titled “The UNREGISTERED_FAILURE Method”We discovered that sendReceipt reveals the true registration status:
// Returns SUCCESS for active users// Returns UNREGISTERED_FAILURE for abandoned accountsconst result = await sendRequest('sendReceipt', { recipient: uuid, type: 'read', targetTimestamp: [Date.now()]});Response types:
SUCCESS- User is registered and activeUNREGISTERED_FAILURE- Account no longer exists on Signal
Scripts
Section titled “Scripts”find-unregistered-users.js
Section titled “find-unregistered-users.js”Located at /app/scripts/find-unregistered-users.js
Comprehensive tool for detecting and managing abandoned accounts.
# Scan all users and save results (cached for later operations)node find-unregistered-users.js --scan --limit 3000
# List cached scan resultsnode find-unregistered-users.js --list
# Remove unregistered users from all groups (uses cached results)node find-unregistered-users.js --remove
# Add unregistered users to Entry/INDOC for moderator reviewnode find-unregistered-users.js --to-indoc
# Add unregistered users to custom verification groupnode find-unregistered-users.js --add-to-group "group-id-here"
# Scan and remove in one commandnode find-unregistered-users.js --scan --removeHow It Works
Section titled “How It Works”- Scan Phase: Queries database for users with active group memberships
- Check Phase: Sends read receipt to each user, checking for
UNREGISTERED_FAILURE - Cache Phase: Saves results to
/app/data/unregistered-users-YYYY-MM-DD-HHMM.json - Action Phase: Can remove users from groups or add to verification group
Cached Results Format
Section titled “Cached Results Format”{ "scan_date": "2025-12-24T17:38:50.948Z", "total_checked": 2144, "total_unregistered": 13, "unregistered_users": [ { "uuid": "d6292870-2d4f-43a1-89fe-d63791ca104d", "display_name": null, "profile_name": null, "group_count": 30, "reason": "UNREGISTERED_FAILURE" } ]}trust-all-users.js
Section titled “trust-all-users.js”Bulk trust all known users to accept their identity keys:
node trust-all-users.jsnode trust-all-users.js --create-group # Add failed users to Entry/INDOCNote: This catches users whose identity keys are invalid, but misses unregistered accounts. Use find-unregistered-users.js for complete detection.
remove-failed-users.js
Section titled “remove-failed-users.js”Remove users who fail the trust check from all groups:
node remove-failed-users.js --limit 5 # Test with 5 usersnode remove-failed-users.js --limit 100 # Remove up to 100list-failed-users.js
Section titled “list-failed-users.js”List users who fail the trust check with their group counts:
node list-failed-users.jsRecommended Workflow
Section titled “Recommended Workflow”Initial Cleanup
Section titled “Initial Cleanup”# 1. Trust all users (catches invalid keys)node trust-all-users.js --create-group
# 2. Find truly abandoned accountsnode find-unregistered-users.js --scan --limit 5000
# 3. Review resultsnode find-unregistered-users.js --list
# 4. Remove abandoned accountsnode find-unregistered-users.js --removePeriodic Maintenance
Section titled “Periodic Maintenance”Run monthly or quarterly:
# Quick scan of active usersnode find-unregistered-users.js --scan --removeVerification Group Workflow
Section titled “Verification Group Workflow”For manual review before removal:
# 1. Scan and add to Entry/INDOC for moderator reviewnode find-unregistered-users.js --scannode find-unregistered-users.js --to-indoc
# 2. Review users in Signal (check if they respond)
# 3. Remove confirmed abandoned accountsnode find-unregistered-users.js --removeTechnical Details
Section titled “Technical Details”Why Trust Passes but User is Unregistered
Section titled “Why Trust Passes but User is Unregistered”Signal’s architecture:
- Identity Store: signal-cli caches identity keys locally
- Registration Service: Signal servers track active registrations
- Trust Command: Only checks local identity store
- Send Operations: Check actual registration status
When a user deletes their account:
- Their identity keys remain in signal-cli’s cache
- Trust command succeeds (keys exist)
- Send operations fail with
UNREGISTERED_FAILURE
Rate Limiting
Section titled “Rate Limiting”The scripts include delays to avoid overwhelming signal-cli:
- 50ms between registration checks
- 100ms between group operations
For large communities (2000+ users), expect full scans to take 3-5 minutes.
Database Updates
Section titled “Database Updates”When removing users, the scripts:
- Call
updateGroupwithremoveMemberto remove from Signal group - Update
signal_member_group_memberships.is_active = falsein database
This keeps the database in sync with actual group state.
Troubleshooting
Section titled “Troubleshooting””No cached results found”
Section titled “”No cached results found””Run a scan first:
node find-unregistered-users.js --scanUser Not Detected as Unregistered
Section titled “User Not Detected as Unregistered”Some edge cases:
- User just deleted account (may take time to propagate)
- Network issues during check
- Rate limiting from Signal servers
Re-run the scan to double-check.
Permission Errors
Section titled “Permission Errors”Ensure scripts are owned by the node user:
docker exec -u root signal-bot chown node:node /app/scripts/*.jsSee Also
Section titled “See Also”- Member Onboarding - New member verification process
- Configuration Reference - Environment variables
- Self-Hosting Guide - Deployment setup