Privacy
Every network connection the app can make.
Stalkrr records meetings, calls, your screen, and (optionally) your messages. The only
honest privacy policy for software like this is one you can check against the code, so
this document lists every network connection the app can make, what crosses it, what
turns it on, and where it lives in the source. If you find any byte leaving the machine
that this page doesn't account for, that's a security bug — report it per
SECURITY.md.
Default posture: nothing you captured leaves this Mac. Out of the box, transcription
runs on-device (vendored whisper.cpp), extraction runs on a local model (auto-managed
Ollama on localhost), and the archive is a local SQLite file under ~/.stalkrr/.
Outbound connections that can carry captured data
These exist only if you explicitly select a cloud provider (an explicit choice in Preferences, never a default — the deliberate friction is the point), and they receive only what the stage needs:
| What is sent | Where | When | Code |
|---|---|---|---|
| Meeting audio (transcoded m4a) | api.groq.com or api.openai.com |
Only when you set the transcription engine to Groq/OpenAI in Preferences (default: local Whisper). The Preferences card says "audio is uploaded" whenever this is on. | app/Sources/StalkrrPipeline/CloudWhisperTranscriber.swift |
| Meeting audio (transcoded m4a) | api.soniox.com |
Only when you set the transcription engine to Soniox (the card says "audio is uploaded"). The uploaded file is deleted from their storage right after transcription. | app/Sources/StalkrrPipeline/SonioxTranscriber.swift |
| Redacted transcript text (post-PII-redaction) | api.anthropic.com, api.openai.com, generativelanguage.googleapis.com, or api.groq.com |
Only when you set the text/vision provider to that cloud (default: local Ollama). | app/Sources/StalkrrPipeline/Providers/Providers.swift, Providers/Vision.swift |
| Redacted transcript text (text-extraction backup) | same as above | Only when "If local AI fails" is set to a named provider in Settings (default Off). | app/Sources/StalkrrApp/BackupExtractor.swift |
| Screen/camera frames (JPEG) — the whole display or a webcam still, not redacted | same as above | Only when you explicitly select a cloud vision provider (default: local Ollama, nothing uploaded). | app/Sources/StalkrrPipeline/Providers/Vision.swift |
| Meeting audio (transcoded m4a) | api.stalkrr.com (Stalkrr Cloud) |
Only when the transcription engine is Stalkrr Cloud — a paid plan you subscribe to and then choose in Preferences (the card says "audio is uploaded"). The gateway routes each language to the engine measured best for it and keeps nothing once the transcript is back — see "Stalkrr Cloud" below. | app/Sources/StalkrrPipeline/CloudWhisperTranscriber.swift (engine stalkrr in app/Sources/StalkrrApp/main.swift) |
| Redacted transcript text (post-PII-redaction) | api.stalkrr.com (Stalkrr Cloud) |
Only when the text provider, or "If local AI fails", is Stalkrr Cloud. Forwarded to an open-weight model at a host with no-training / no-logging terms; not kept. | app/Sources/StalkrrPipeline/Providers/Providers.swift (makeStalkrrExtractor) |
The redaction stage (app/Sources/StalkrrPipeline/Redact.swift) masks PII in transcript
text before it is sent. It does not touch images — frames are sent as captured.
Outbound connections that never carry captured data
| Purpose | Where | What is sent | Code |
|---|---|---|---|
| Daily usage report | stalkrr.com/api/pulse |
One JSON POST under 1 KB, at most once per UTC day: version, macOS, chip family, RAM bucket, engine/providers/toggles, activity and performance as named buckets, crash and warning counts. No identifiers, no content, no free text. The complete accepted field list is served at GET https://stalkrr.com/api/pulse. Toggle: Settings → "Send one anonymous report a day" (on by default; off counts nothing). |
app/Sources/StalkrrCore/UsageReport.swift, app/Sources/StalkrrApp/UsageReporter.swift, site/functions/api/pulse.js |
| License refresh (Pro / Cloud keys only) | api.stalkrr.com |
Only when a Pro or Cloud key is present — never on a free install. Once a day for subscription keys, once a week for lifetime keys (a revocation check): the key itself (a signed blob that carries the buyer's name and email), a random per-install id, the app version, and the languages set in Transcription — so a subscription key can be re-minted with a later expiry, and a refunded key revoked. Removing the key in Settings → Plan ends the call. | app/Sources/StalkrrApp/LicenseManager.swift |
| Update check | github.com (StalkrrOS/stalkrr-releases) |
A plain GET for appcast.json; downloads the signed update zip. No identifiers. Always on: updates install themselves and there is no switch in the UI (defaults write com.stalkrr.app autoUpdate -bool false disables it). |
app/Sources/StalkrrApp/UpdateChecker.swift |
| Whisper model download | github.com (StalkrrOS/stalkrr-releases, self-hosted weights) or huggingface.co (the Japanese/Chinese specialists) |
A GET for the model file, once per model. | app/Sources/StalkrrPipeline/WhisperTranscriber.swift, LanguageModelRegistry.swift |
| Speaker diarization models | github.com (StalkrrOS/stalkrr-releases, models-2) |
One GET for a 19 MB zip, once; verified against a pinned sha256 before anything is unpacked. Toggle: Settings → Speaker diarization. | app/Sources/StalkrrPipeline/SpeakerModel.swift |
| Local-AI runtime download | github.com (ollama releases) + the Ollama model registry |
GETs for the pinned Ollama build and the local model, once. | app/Sources/StalkrrApp/OllamaManager.swift |
| Self-hosted inference (optional) | your own localhost/LAN URLs for Ollama or vLLM |
Redacted text, to servers you run. | endpoint prefs in app/Sources/StalkrrApp/PreferencesWindow.swift |
Things that are local by construction
- Transcription default: whisper.cpp runs in-process (Metal/Core ML). The fallback
Apple engine sets
requiresOnDeviceRecognition = true(app/Sources/StalkrrPipeline/Transcriber.swift) — dictation audio never goes to Apple. - The UI cannot reach the network. Every window is a WKWebView locked with
Content-Security-Policy: default-src 'none'(app/Sources/StalkrrApp/WebUI/*.html) and served from a custom local scheme (app/Sources/StalkrrApp/Web/) that only reads bundled assets and your own media files. - The MCP server is stdio-only (
app/Sources/StalkrrMCP/) — your agent reads your memory through a local pipe, not a port. - Imported recordings (files you drop on the window or pick with Import…) are
copied into
~/.stalkrr/media/<id>/— the original file is never moved or modified — and follow the same retention window as captured audio; a movie contributes only its audio track. Transcript and memory rows persist like any recording's. - The people directory — the names you tag on recordings, and which transcript speaker is who — lives in the same SQLite file as everything else. Names never leave the Mac; deleting a person un-tags them everywhere, and Erase all clears the directory. Voice data (0.9.1): with speaker diarization on, each diarized speaker gets a 256-number voice centroid (1 KB) stored per recording in that same file; voice recognition compares those locally to suggest a name (never to tag on its own) and nothing about them ever leaves the Mac. Deleting a person detaches their voices; erasing a recording or everything removes them. The diarization models run on the Neural Engine and come, once, from Stalkrr's own release (see the outbound table) — never from a third-party host at runtime.
- WhatsApp (0.10.0): when switched on, Stalkrr reads WhatsApp for Mac's own message
database (
~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite) in place and read-only — never modified, never sent anywhere — and only the chats on your list (whitelist) or not on it (blacklist). Texts and voice-note transcripts go into the same SQLite file as everything else, filed under the person; voice notes are decoded on this Mac and kept as small m4a copies under~/.stalkrr/media/whatsapp/with the usual retention. Received notes may enrol the sender's voice (see voice data). Forgetting a person's WhatsApp history removes all of it; Erase all clears the tables. Stalkrr does not connect to WhatsApp's servers for this. - One report leaves, and it is not about you. No crash reporting, no session
tracking, no identifier of any kind. Once a day the app posts about two dozen bucketed
facts about the software — version, macOS version, chip family, which engine and
providers are selected, how much was recorded in coarse ranges, how fast it ran, how
many times it crashed — to
stalkrr.com/api/pulse. No content, no names, no identifiers, nothing that ties two days' reports to the same Mac. Settings → Send one anonymous report a day switches it off. The full field list, the example payload, and what makes "no personal data" true are in The daily usage report. - Storage: recordings, transcripts, and memory live in
~/.stalkrr/(SQLite + media files, plus a thumbnail cache under~/.stalkrr/cache/thumbs/); API keys live in the macOS login keychain. - Erase means erase. Deleting a recording (Timeline → Delete, or the MCP tool) or
everything (Erase all) removes the database rows in one transaction, then the files the
database recorded for it — verified, and reported honestly: the app tells you if a file
could not be removed and finishes the job at the next launch
(
app/Sources/StalkrrMemory/MeetingEraser.swift). A deleted recording is tombstoned so work still running in the background cannot recreate it and a crash cannot resurrect its audio. The database runs withsecure_delete(erased rows are overwritten) and is vacuumed after Erase all, so erased content is gone from the file, not just from queries. Caveat honesty: SSD wear-levelling means no software can promise that old blocks are physically unrecoverable — FileVault is the answer to that.
The daily usage report
Stalkrr is a one-person project with no accounts, no crash reporter and no support queue, so until 0.12.0 nobody knew whether an update installed, whether transcription was fast enough on an M1, or whether anything crashed. The daily usage report is the smallest thing that answers those questions. It is on by default — Settings → Send one anonymous report a day turns it off, and off means nothing is counted — and this is all of it:
{
"archive_gb": "1-5",
"capture_warnings": 1,
"chip": "m4",
"crashes": 0,
"days_covered": 1,
"engine": "whisper",
"extract_cloud": 0,
"extract_failed": 1,
"extract_local_ok": 5,
"extract_wall": "15-60",
"imported": "0",
"langs": "en,he",
"local_model_gb": "5-10",
"mem_pressure": 2,
"minutes": "121-480",
"os": "15.6",
"ram_gb": "32",
"recordings": "3-5",
"rtf": "5-10",
"text_provider": "ollama",
"thermal": "fair",
"toggles": "coreml,diarize,login,mcp_destructive,whatsapp",
"v": 1,
"version": "0.12.0",
"vision_provider": "ollama",
"whisper_model": "auto"
}
That is a whole report. There is no other one.
No personal data, ever — what makes that true, in the order it stops being a slogan:
- No identifier. No install ID, no device ID, no serial, no account, no cookie, no hash of anything. Nothing in the report is derived from your machine's identity, so two reports cannot be joined — not by us, not by anyone reading the dataset.
- No content. No titles, no transcripts, no file names, no app names, no people, no search terms. Counts, ranges and speeds only.
- No free text. Every string is a member of a closed list written into the code
(
app/Sources/StalkrrCore/UsageReport.swift), a fixed number pattern (0.12.0,15.6), or a language code Whisper can produce. There is no field a name could be typed into, deliberately; anything that is not on a list becomesother. - The app can only send this. The self-test (
swift run stalkrr-selftest) asserts the report's exact key set and feeds it hostile inputs — a title, an email address, a file path, an API key — and checks none of it survives. Adding a field without updating that list fails the build. - The server can only store this.
site/functions/api/pulse.jsbuilds the stored row by walking its own schema, never the request. An unknown key, a wrong type, a number out of range or a string not on its list is rejected and nothing is written. The schema it enforces is the schema it publishes atGET https://stalkrr.com/api/pulse— the same file serves both, so the promise and the enforcement cannot drift apart. - Nothing about the connection is kept. Cloudflare terminates the connection and so sees your IP, exactly as it does when you load any page on any site behind it. The Function never reads it. What it reads is the two-letter country Cloudflare derives at the edge, and that is the only thing the server adds to the report.
- The log line before every send. The exact bytes are written to
~/.stalkrr/stalkrr.log(usage: {…}) before they leave; Settings → "Show what would be sent" shows the same document. The server's own log is a count —pulse ok— never a body and never an address. - It is thrown away. Rows live in Cloudflare Workers Analytics Engine for three months and are then deleted. There is no backup.
The honest caveat. Any set of buckets is, in combination, a rough description of a configuration — an M4 with 32 GB running Whisper in English and Hebrew is not one Mac in the world. The buckets are coarse for exactly that reason: chip family and not chip model, six memory buckets, at most three languages, ranges and not counts. What is not there — and it is the part that matters — is anything that makes two reports from the same Mac more linkable than two reports from any two Macs set up alike. We cannot count you twice, and we cannot count you once.
What it is used for. Deciding what to build and what to fix: is anyone on an old
version, is transcription fast enough on the machines people actually have, which
languages need a model, did that release crash. The numbers are read with
python3 site/stats.py; docs/ANALYTICS.md is the map of everything counted.
Stalkrr Cloud (the paid hosted processing)
Stalkrr Cloud is optional and off unless you subscribe and then choose it in Preferences. When it is on:
- What leaves the Mac is exactly the two Stalkrr Cloud rows in the first table: compressed audio for transcription, and PII-masked transcript text (with the speaker names you tagged) for extraction. Screen and camera frames never go through it — there is no cloud vision through the gateway.
- Nothing is stored. The gateway streams each request to the engine that handles it and returns the answer. Audio bound for a Stalkrr-run engine sits in temporary storage only for the duration of that job and is deleted when it completes (a one-hour lifecycle rule is the backstop). Transcripts and extractions are not kept. Logs carry status codes and sizes, never content.
- Who processes what. Transcription is routed per language to the engine measured best for it — a Whisper specialist run by Stalkrr (Hebrew, Japanese and Chinese today), Microsoft's MAI-Transcribe-2 on Azure Speech, or Soniox — and every hosted engine is used only under no-training and no-logging terms; Settings → Transcription shows which engine handles each of your languages. Extraction runs on an open-weight model at a host under the same terms. No provider hosted in China is used. Payments are handled by Paddle (the merchant of record) and the key email by Resend. Sub-processors: Cloudflare (the gateway), the GPU host of the specialists, Microsoft Azure, Soniox, the LLM host, Paddle, Resend.
- What Stalkrr keeps about a subscriber: the license record (name, email, plan, expiry), a random per-install id, and usage counters (audio seconds, tokens, timestamps) for the fair-use quota — never content.
- The key carries your name and email inside a signed blob so Settings can show whose it is;
it is refreshed once a day (see the table above). Removing it ends every contact with
api.stalkrr.com.
How to audit this yourself
- Enumerate every URL in the code:
bash grep -rhoE "https://[a-zA-Z0-9./_-]+" app/Sources --include="*.swift" | sort -uCompare against the tables above. (URLSession is the only HTTP client; there are no third-party networking dependencies to hide in — the dependency list isapp/Package.swift, and the single vendored library is whisper.cpp.) - Build it yourself (
cd app && swift build && ./scripts/build-app.sh) and run your own bundle — seeapp/README.md. - Watch it live with Little Snitch/LuLu: with default settings you should see the
update check, one-time model downloads, and one small POST to
stalkrr.coma day — all three listed above, and nothing else. Point a proxy at that POST if you want to read it; it is plain JSON and it says what it is.
The website
stalkrr.com is static files on Cloudflare Pages plus two small functions: /dl, which
resolves the current release download, and /api/pulse, which receives the daily usage
report described above. Those are the only times the app talks to this domain — never
for updates (those come from GitHub), never for models, never for anything you captured.
The site itself uses Cloudflare Web Analytics, which records page views (path,
referrer, country, browser family) without cookies, without any identifier, and without
storing anything on your device, so there is no consent banner because there is nothing
to consent to. Unsampled data is kept for 7 days, aggregates for 6 months. The Download
button leads to /download, so that page's views are the download count. Links we post
elsewhere may carry a path such as /from/x, which serves the same home page, so the
path tells us which place brought you — that, and the referrer your browser sends anyway,
is the whole of the "tracking": no cookies, no identifiers, no query strings, no pixels. The pages load their typefaces from Google Fonts and nothing
else from anywhere else; the site's Content-Security-Policy header lists every host a
page is allowed to talk to, and the build that produces it is site/build.py.
Privacy policy
Effective September 2026. The sections above are the technical description of what the app sends; this section is the policy that goes with it.
Who is responsible. Stalkrr is an independent product operated from Israel ("we"). Contact for anything in this document, including requests about your data: info@stalkrr.com.
The app on your Mac. Recordings, transcripts, extracted memory, the people directory and
voice data are processed and stored on your Mac only, under ~/.stalkrr/. We never receive
them. You are the controller of what you record; the in-app Terms put the right to record the
people you capture on you.
What we collect, and why.
- The website (stalkrr.com): Cloudflare Web Analytics counts page views by path, referrer, country and browser family, without cookies or identifiers (see "The website" above). Legal basis: our legitimate interest in knowing whether the site works.
- The anonymous usage report (if left on): the document described above — no identifier, no content. Legal basis: legitimate interest in keeping the app working across Macs; switch it off in Settings.
- Purchases (Pro and Cloud): Paddle.com Market Ltd, our merchant of record, collects your name, email, country and payment details to sell you the plan, charge tax and issue invoices, under Paddle's privacy policy. Paddle sends us your name, email, the plan, and the transaction and subscription ids; we mint your license key from them and keep that record (the license record) for as long as the plan can be renewed or refunded, then as long as tax rules require of Paddle's invoices. Legal basis: the contract.
- License keys: a Pro or Cloud key carries your name and email inside a signed blob so the
app can show whose it is. When a key is present the app calls
api.stalkrr.comonce a day with the key, a random per-install identifier and the app version, so a subscription key can be renewed and a refunded one revoked. We keep the install identifiers and last-seen dates with the license record. A free install makes no such call. Legal basis: the contract. - Stalkrr Cloud (if you subscribe and switch it on): compressed audio for transcription and
PII-masked transcript text for extraction pass through
api.stalkrr.comto the processing engines listed in "Stalkrr Cloud" above, are not stored, and are not logged; we keep usage counters (audio seconds, tokens, timestamps) per license for the fair-use quota, for 13 months. Legal basis: the contract. The engines process your audio and text on our instructions only, under terms that forbid training on it or retaining it. - Email: the key email after a purchase (and on request from stalkrr.com/key) is sent by Resend on our behalf; support email you send us is kept as long as the conversation needs.
Who else sees data. Only the processors named here: Cloudflare (the website, the gateway and its database), Paddle (payments), Resend (the key email), and — for Cloud only — the transcription and extraction engines listed above (Microsoft Azure, Soniox, the GPU host of Stalkrr's own Whisper specialists, and the open-weight model host). We sell nothing to anyone and run no advertising.
Where. These processors run in the United States and the European Union; Cloudflare processes at the edge nearest to you. Transfers rely on the processors' standard contractual clauses. Nothing about you is hosted in China.
Your rights. You can ask what we hold about you, have it corrected or deleted, and object to processing — email info@stalkrr.com; we answer within 30 days. Deleting a license record ends the plan. Everything the app holds is yours to erase from the app itself (Timeline → Delete, or Erase all captured data). If you are in the EU/EEA or the UK you can also complain to your data-protection authority.
Children. Stalkrr is for people aged 18 and over.
Changes. We update this document when the software changes what it sends; the version of PRIVACY.md in the repository is the record of what applied when. Material changes to the policy are announced in the app.