Privacy¶
Wristotle is built around one principle: everything stays on your phone.
No cloud, no telemetry, no accounts, no upload. Reading this page top-to-bottom should give you a complete map of what data exists, where it lives, and how to wipe it.
What stays on-device¶
| Data | Where | Notes |
|---|---|---|
| Whisper speech recognition | On your phone, via whisper.cpp | Models downloaded once from HuggingFace at your request, then inference is local. No audio sent over the network in the default Local-only Speech-provider mode — see the cloud-STT entry in What leaves your phone for the opt-in. |
| Intent classification | On your phone, via ONNX Runtime + MiniLM-L6-v2 | Same pattern — model is downloaded once from HuggingFace, inference is local. |
| Contacts lookup | On your phone, via the Android ContactsProvider | Read-only READ_CONTACTS permission. No contact data leaves the app. |
| Conversation history | On your phone, Room SQLite at /data/data/com.lazydevs.wristotle/databases/wristotle_conversation.db |
App-private storage — not accessible to other apps. Auto-pruned by your retention window. |
| Learned intent examples | Same — separate Room DB (wristotle-nlu.db) |
Successful commands optionally added to the intent classifier's example bank (toggle in Settings → Learning → Intent). The full transcript of each phrase is stored verbatim (so "call John Smith" keeps the name in the bank). View + delete per-row via Settings → Learning → Saved phrases. |
| Installed-app index | Same — separate Room DB (wristotle-app-index.db) |
Populated on demand from PackageManager. Used to resolve "open Spotify" → com.spotify.music. Never transmitted. |
| Dictation audio (opt-in) | App-private storage at filesDir/conversation-audio/, max 5 most recent |
Off by default. WAV files. |
| Permission state, model state, settings | SharedPreferences | App-private. |
What leaves your phone¶
Seven things, all initiated by you, with no background activity:
- Whisper model downloads — HuggingFace (
huggingface.co/ggerganov/whisper.cpp/resolve/main/...). Done once per model on user tap. Resume-able if interrupted. - MiniLM intent model download — same pattern, from HuggingFace.
- Outbound calls + SMS — when you ask the watch to call or text someone, the app routes through Android's
TelecomManager/SmsManager— same path any other Android app uses to place a call or send a text. The call or SMS itself goes through your carrier as normal; Wristotle never sees the conversation. - Backups you export — when you tap Settings → Backup & Restore → Export, the ZIP is written to wherever you pick (Drive, OneDrive, a local Files folder, etc.). What ends up in cloud storage is whatever you chose; Wristotle never uploads anything by itself.
- Speech-to-text via cloud / self-hosted endpoint (opt-in) — only when you set Settings → 🧠 Models → Speech provider to anything other than the default "Local Whisper only." Each utterance's 16 kHz mono WAV is POST'd to the OpenAI-compatible
/audio/transcriptionsendpoint you configured (Groq, OpenAI, Cloudflare Workers AI, a self-hosted Speaches or whisper.cpp HTTP server on your homelab, …). Wristotle has no opinion about whether that endpoint logs or retains your audio — that's the provider's policy, not ours. Pick a provider you trust, or point at a server you control. The default mode is Local only, so by default dictation audio never leaves the phone. - Weather queries — only when you voice a weather command ("what's the weather" / "weather in Tokyo"). The phone makes a direct HTTPS request to your chosen weather provider; the Weather queries section below spells out the exact payload.
- AskAgent + MCP queries — only when you voice an "ask agent …" / "ask claude …" command AND you've configured an LLM API key. The phone makes a direct HTTPS request to your chosen LLM provider, and (if you've enabled any MCP servers) to those servers' endpoints during the tool-calling loop. The AskAgent queries section below spells out the exact payload.
That's it. No analytics, no crash reporters, no usage stats, no "phoning home" of any kind.
The Android manifest is the source of truth — INTERNET covers the model downloads + on-demand weather and AskAgent queries; ACCESS_COARSE_LOCATION is optional and used only by the bare "what's the weather" command. You can audit them in the source (Wristotle Companion repo).
Weather queries¶
The first voice command that calls out to the network (Sports, AskAgent, and Home Assistant, documented below, also do). Triggered only when you ask — there's no background polling, no daily fetch, no widget refresh.
What's sent¶
Per query, one or two HTTPS requests to your chosen provider:
| You said | Sent to the provider |
|---|---|
| "weather in Tokyo" | the spoken place string (tokyo), then the resolved lat/lon |
| "what's the weather" (no city) | your phone's last-known lat/lon (only if you granted Location; otherwise the watch just shows a hint) |
Nothing else accompanies the request — no device ID, no account, no IP-besides-the-network-layer-default, no installed-app list, no contacts. Each request is independent; the provider can't link two of your queries to each other.
When you've selected OpenWeather in Settings → Weather and pasted your free-tier API key, that key rides along on every request (that's how OpenWeather meters its free tier). The key is stored locally in app-private SharedPreferences and never sent anywhere except the openweathermap.org endpoint.
Which provider¶
| Provider | Default? | API key | Endpoints |
|---|---|---|---|
| open-meteo.com | Yes | None — no registration | geocoding-api.open-meteo.com + api.open-meteo.com |
| openweathermap.org | Opt-in | Free-tier key you paste in Settings → Weather | api.openweathermap.org |
Both honor their respective privacy policies; neither requires you to identify yourself beyond a network-layer IP.
Opting out¶
The simplest way: don't say weather commands. There's nothing to disable — the network call only happens at the moment you ask.
For the bare "what's the weather" form: revoke Location in the Permissions tab. The location-qualified form ("weather in Tokyo") keeps working.
Sports queries¶
Like Weather, sports commands call out to the network — and only when you ask. No background polling, no score-ticker, no daily fetch.
What's sent¶
Per query, one or more HTTPS requests carrying only what the query needs:
| You said | Sent |
|---|---|
| "when do the Warriors play next" | the team you named (resolved to a public team id) and the league |
| "NBA standings" | the league slug only |
No device ID, no account, no contacts, no installed-app list — each request is independent and can't be linked to another. Your favorite teams and sport priority are resolved on-device; they're never uploaded.
Which provider¶
| What | Host | Key |
|---|---|---|
| Live scores / schedules / standings | ESPN's public sports API (site.api.espn.com) |
None — no registration |
| Team-list / endpoint config (best-effort, cached ~24 h) | wristotle.codeberg.page |
None |
The team-list config keeps the feature current without an app update; if it can't be reached, a copy bundled in the app is used instead, so name resolution still works offline.
Opting out¶
There's nothing to disable — the network call only happens at the moment you ask. The simplest opt-out is to not say sports commands.
AskAgent queries¶
An opt-in voice command that calls out to the network. Triggered only when you say "ask agent …" or "ask claude …" (or one of the other aliases: "ask the agent …", "hey agent …", or — from v0.16.0 onward — any custom trigger word you've added under Settings → ✨ Ask Agent, either as a verb-form "ask jarvis …" or as a bare wake word "Jarvis, …"). Until you configure an LLM provider in Settings → ✨ Ask Agent, the command answers locally with "No agent key set" and nothing leaves the phone.
What's sent¶
Per query, one HTTPS request to the LLM provider you configured (or up to five rounds when MCP tools are in play — see below):
| Field | Value |
|---|---|
| The user query | Your transcribed sentence, with the trigger lead-in ("ask agent", "hey claude", your custom "jarvis", etc.) stripped |
| The system prompt | Whatever's in Settings → ✨ Ask Agent → System prompt (the pre-filled watch-friendly baseline by default — you can edit or clear it) |
| The model id | claude-sonnet-4-6 / gpt-4o-mini / whatever you set |
| API key | In the request header (Anthropic: x-api-key; OpenAI-compatible: Authorization: Bearer ...) |
| The MCP tool list | When any MCP server is enabled, the names + JSON schemas of every tool from every enabled server. Tool names are wire-prefixed with the integration name (github__get_me). |
| MCP tool results | When the LLM picks a tool, its arguments are forwarded to the matching MCP server (e.g. api.githubcopilot.com/mcp/). The tool's response is then sent back to the LLM as the next message in the loop. |
Nothing else accompanies the request — no device ID, no account, no contacts, no installed-app list. Each query is independent.
Which provider¶
Two wire shapes are supported; the endpoint URL determines the destination:
| Provider | Wire shape | Endpoint | API-key flow |
|---|---|---|---|
| Anthropic (Claude) | Anthropic Messages | api.anthropic.com/v1/messages (hard-coded) |
Paste your key from console.anthropic.com → API Keys |
| OpenAI-compatible | OpenAI Chat Completions | Whatever URL you paste — OpenAI itself, OpenRouter, Ollama, llama.cpp, vLLM, … | Paste a key for the chosen service (or leave blank for local servers that need no auth) |
API keys are stored on the phone in app-private SharedPreferences and only sent on the matching provider's requests. Switching providers doesn't leak keys between them — each is held in its own preference slot.
MCP servers¶
If you add HTTP MCP servers under Settings → 🔌 MCP servers, the AskAgent loop can call their tools. Each tool call is a direct HTTPS request from your phone to the MCP server's URL, with the Authorization header value you provided. The companion holds no other state about the server beyond what you typed (name, URL, optional auth header, transport).
Opting out¶
The simplest way: don't say "ask agent" / "ask claude" commands.
To make the intent inert without removing the API key: clear the system-prompt field or change the prompt — but the simplest disable is to clear the API key field. With no key, AskAgent answers "No agent key set" and nothing is sent.
To disable MCP tool calling but keep plain LLM Q&A: delete or toggle off the MCP servers in Settings → 🔌 MCP servers. With no servers enabled, AskAgent falls back to the one-shot LLM call (no tool list, no agent loop).
Home Assistant commands¶
An opt-in voice command that calls out to the network — but only ever to your own Home Assistant server. Triggered only when you say "home assistant …" / "hey home assistant …" / "tell home assistant to …" (or a custom wake word you add under Settings → 🏠 Home Assistant). Until you enter both the base URL and an access token, the command answers locally with "Home Assistant not set up." and nothing leaves the phone.
What's sent¶
Per command, one request to the base URL you configured, at the path /api/conversation/process:
| Field | Value |
|---|---|
| The command text | Your transcribed sentence, with the wake-word lead-in stripped |
| Language | The language code from Settings (default en) |
| Access token | In the Authorization: Bearer … header |
Nothing else accompanies the request — no device ID, no account, no contacts, no installed-app list.
Which destination¶
Only the Home Assistant instance whose base URL you entered. There is no third party and no Wristotle server in the path — the request goes straight from your phone to your server over whatever network reaches it (LAN, Tailscale, VPN, or the public internet if that's how you expose HA). Home Assistant runs its own natural-language understanding and acts on your devices; there's no LLM and no cloud relay on Wristotle's side. The access token is stored on the phone in app-private SharedPreferences and is sent only on requests to your configured URL. It rides in a backup only if you tick the Home Assistant token secret on export.
Opting out¶
Don't say Home Assistant commands, or clear the base URL / token field in Settings → 🏠 Home Assistant — with either blank, the command answers "Home Assistant not set up." and no request is made.
The optional dictation audio capture¶
By default, the raw WAV of each dictation is NOT saved. Wristotle deletes the audio buffer the moment the transcript is produced.
If you enable Settings → Conversation → Audio → Save dictation audio, each dictation's WAV is written to app-private storage. Only the 5 most recent are kept; older ones are auto-deleted. The chat tab shows an inline play button on rows whose audio is still on disk.
This is a microPebble-only feature, and the toggle is hidden under Core Devices: capture works by saving the bytes as Wristotle's on-device Whisper transcribes them, which only happens on the microPebble path. Under Core Devices, dictation runs in Core Devices' own engine and only the finished text reaches Wristotle — there's no audio for Wristotle to save.
Why off by default: a transcript leaks the words you said. A recording leaks your voice, your environment, anyone else who happened to be talking nearby. Different threat model — different consent.
To wipe immediately: same card → Delete saved audio (button does not toggle the capture preference itself, so you can keep capture on and only clear past recordings).
Backups you export¶
Settings → Backup & Restore → Export writes your data to a ZIP at a location you choose with the system Save-As dialog (Drive, OneDrive, a local Files folder — wherever). What lands in the ZIP:
- Notes (text + optional audio recordings of dictated notes)
- Tasks (text + completed state)
- Conversation history (text + optional dictation audio)
- Learned intent phrases
- App aliases
- Contact aliases (phrase → contact phone number + name snapshot; see Settings → Learning → Contact aliases)
- Reminders pending on the watch
- Settings (the user-tunable preferences from each Settings card)
What's not in the ZIP:
- The Whisper / MiniLM model files themselves (large and re-downloadable from the Models card). Your active model selection IS recorded so the Models card prompts you to redownload after restore.
- The app-launcher index. Regenerated on demand via Settings → Apps → Scan installed apps.
Encryption. The Export dialog has an optional password field. If you set one, the ZIP is encrypted with AES-256 (a standard format readable by 7-Zip, Keka, WinRAR, etc.). Empty password = plain ZIP. If you set a password and lose it, the backup cannot be restored — write it down somewhere safe. There is no recovery path; Wristotle doesn't store the password.
Audio. The Export dialog also has an "Include audio recordings" toggle. Off by default. When on, every WAV under your notes-audio and conversation-audio directories is added to the ZIP. Backups can be hundreds of MB with audio included; without audio, typically under a megabyte.
On a cloud drive. A backup written to Google Drive / OneDrive / iCloud is now in whatever encryption + access regime that service provides. If you care about that data, use the password option so the ZIP itself is encrypted before it leaves your phone.
Restore. Restoring a backup never deletes existing local data. It dedupes by natural identity (notes by created-time + body, tasks by created-time + text, conversations by timestamp + query + response, learned phrases by their normalised text) and skips entries that already exist. Settings prefs and aliases (app + contact) are overwritten with the backup's values. Contact aliases also go through a relink pass against the new device's Contacts — dead lookup keys get re-bound by exact-name then by phone number, so an alias you created on your old phone follows you to the new one. The result dialog shows a per-entity table (Total · Imported · Duplicates · Failed) so you see exactly what changed.
The conversation history¶
Every command-and-response pair gets a row in the Conversation tab. By default the retention window is 10 days — anything older is automatically pruned.
Adjustable in Settings → Conversation → History → Retention (1 / 10 / 20 / 30 days). Shortening always asks for confirmation and tells you exactly how many entries would be deleted.
Wipe immediately: Settings → Conversation → History → Clear all. Removes all rows AND any associated saved audio.
The bug-report flow¶
The in-app diagnostics export (Settings → Diagnostics) bundles useful debug info into a markdown report you can paste into a public Codeberg issue. By default it redacts contact names, phone numbers, message bodies, and replaces phone-digit runs in log lines with <digits>.
See the Bug reports page for the full list of what's in the bundle and what the redact toggle covers (and doesn't).
Permissions Wristotle asks for and why¶
| Permission | Why |
|---|---|
READ_CONTACTS |
Resolve names in voice commands like "call Mom" |
CALL_PHONE |
Place calls via TelecomManager |
SEND_SMS |
Send text messages via SmsManager |
READ_CALENDAR |
Answer calendar questions like "when is my next meeting" (reads the phone calendar via CalendarContract; nothing leaves the app) |
WRITE_CALENDAR |
Create events for "schedule a meeting…" on your phone calendar |
RECORD_AUDIO |
Capture audio for recognition (the system-wide voice input case — when an external app routes audio to us) |
INTERNET |
Download Whisper + MiniLM model files from HuggingFace |
POST_NOTIFICATIONS |
Foreground-service notification on Android 13+ |
FOREGROUND_SERVICE + FOREGROUND_SERVICE_CONNECTED_DEVICE |
Long-running watch-bridge service |
QUERY_ALL_PACKAGES |
Build the on-device app-launcher index for "open |
BIND_NOTIFICATION_LISTENER_SERVICE (via system Settings) |
Cross-app media control via MediaSessionManager |
Every permission grant is reversible from Android Settings → Apps → Wristotle Companion → Permissions. Revoking any of them just disables the feature that uses it; the rest keep working.
Wiping everything¶
To leave no trace of Wristotle on your phone:
- Clear app data in Android Settings → Apps → Wristotle Companion → Storage → Clear data. Removes all SharedPreferences, all Room databases, all saved audio, all downloaded model files.
- Uninstall the APK.
After uninstall, no Wristotle file remains. The conversation database, the audio recordings, the downloaded models — all gone with the app. Android isolates app-private storage per package; nothing carries forward to a future reinstall.
If you've exported backups elsewhere (Drive, a Files folder, an SD card), those ZIPs are not touched by the uninstall — they live in whatever you chose. Wipe those separately if you want to.