Privacy Policy
Last updated: October 2026
Introduction
Maskeno ("we", "our", or "us") is a local-first tool that finds and masks personal data (PII) in documents before you share them with AI services. This Privacy Policy explains how information is handled when you use the Maskeno desktop application and this website (Maskeno.app). It describes what the app's code currently does.
The short version: PII detection and masking run on your device, and no detection step sends your documents to a server. The app is not a zero-network application: it makes requests for licensing, trial enforcement, model and OCR language-data downloads, Ollama on this machine, optional AI over masked text, updates, and, only after you agree, analytics and feedback. The original document never leaves your device: text extraction and OCR run locally, and there is no cloud OCR. They are itemised below so you can make an informed choice. Detection also misses things, and masked text is not anonymised, so read the masked text before you send it anywhere.
Data Controller
Creatiwi AI
Republic of Poland
Contact: [email protected]
What Stays on Your Device
Maskeno is local-first. The following stays on your machine. The only document content that can leave it is masked text you choose to send with Ask AI (described below):
- Your documents, images, and the text extracted from them
- OCR of scanned pages and images, which runs locally only: with Tesseract built into the app (the default) or with Ollama on this machine. There is no cloud OCR.
- Detected PII spans and redacted output files
- Redaction-quality feedback signals (counts per category, the action you took, no document or span text), stored in the local database and never sent anywhere
- Your projects, stored in a local SQLite database in your user-data folder (see "Data Stored on Your Device")
What Does Leave Your Device
Running the desktop app involves network requests. We list them plainly rather than claiming that no data ever leaves your device:
- Trial registration and status: a machine ID, OS platform and app version at registration; the machine ID for status checks; the machine ID with project and page counts for usage reports. These go to our trial server (a Cloudflare Worker), are not gated by analytics consent, and are not stopped by a license: a valid license lifts the trial limits, but the app still requests trial status and reports usage.
- OCR language data: when you run OCR in a language whose language file is not in the app's language-data cache (the first time, and again after that cache has been emptied), the app downloads the Tesseract language file for that language (for example Polish and English) from the tesseract.js language-data CDN (jsDelivr). The app keeps the cache in the temporary folder of your operating system, which the system or a cleanup tool can empty. It is a plain download request with no document content, machine ID or license key. To keep OCR fully offline, set the
TESSDATA_PATHenvironment variable, in the environment the app is started from, to a folder that holds the gzipped language files, named<lang>.traineddata.gz(for a Polish documentpol.traineddata.gzandeng.traineddata.gz). Plain.traineddatafiles work too, when the folder holds every language the app needs as a plain file and none of them as a.gzfile; do not mix the two forms in one folder. The app does not fall back to the download for a language whose file is missing there. - Licensing: if you enter a license key, the key and the machine ID are sent to Polar.sh to activate the license and, about every 7 days, to re-validate it. Activation also sends the OS platform and app version.
- Detection model downloads: when a preset needs a model that is not on your device yet, the files are downloaded. The secure, legal and medical presets need the nym model (about 376 MB). Legal and medical also need the OPF model (about 917 MB), and secure needs it for text that is not Polish. For Polish text, legal and medical also need the GLiNER2 Polish model (about 395 MB). All three come from models.audior.app, our own storage on Cloudflare R2, and the nym and OPF models fall back to Hugging Face if that host cannot be reached, answers with an error or serves other bytes. The GLiNER2 model has no fallback. Each file is checked against a pinned size and SHA-256 hash when it is downloaded. No document data is sent, but if the environment variable
HF_TOKENis set, it goes to Hugging Face as a bearer token on a fallback download. The developer preset uses pattern recognizers only and downloads nothing. - Ask AI model list and API key check: when the Ask AI model picker has no cached list (the list is cached for an hour) or you refresh it, the app fetches the model list from OpenRouter, with your OpenRouter API key if you stored one. Testing the OpenRouter key in Settings sends that key to OpenRouter. No document data is sent. Not configuring OpenRouter avoids these requests.
- Ollama: the app talks to Ollama on this machine only. It accepts only a loopback address for Ollama (localhost, an address in 127.0.0.0/8, or ::1) and refuses any other. It checks the address when you save it, deletes a stored non-loopback address at every start and checks it again before each OCR run, because OCR sees the original, unmasked document. The requests are status, version and model-list checks, model downloads and deletions, page images when you run OCR with Ollama, and the prompt with masked text when you run Ask AI with Ollama. Status checks run automatically whenever Settings or the Ask AI panel opens, even if you never use Ollama, and there is no setting to turn them off. All of these requests go to that loopback address.
- OCR: nothing leaves your device. OCR is local only: Tesseract built into the app (the default) or Ollama on this machine. There is no cloud OCR, and page images are never sent to a cloud provider.
- Ask AI and prompt packs (optional): your chosen prompt plus the masked project text goes to OpenRouter with your own API key, to Ollama on this machine, to chatgpt.com through a link that carries the text, or wherever you paste a copied prompt. The next section describes what that text contains.
- Crash reporting and usage analytics (opt-in): off by default. After you grant consent in the consent dialog or in Settings, Sentry receives crash reports (error messages and stack traces, tagged with a random install ID that stays the same across launches) and Aptabase receives usage events. No document content is attached on purpose. The app removes Sentry's user and host-name fields. In the main process the text of an error (message, stack, breadcrumbs, tags) goes through a strict scrubber that replaces paths, e-mail addresses, IBANs, long numbers and quoted text, and replaces every word that is not one of the app's own message words with an underscore. Crash reporting in the app window scrubs file paths only. A scrubber is a safety net, not a guarantee. Usage event names and text properties are validated and shortened before they are sent. Withdrawing consent stops new usage events and new main-process crash reports; reports already queued may still be sent, and crash reporting in the app window stays active until you restart the app.
- Feedback (optional): when you submit the feedback form, the app sends a message to a Slack incoming webhook that belongs to our team: your answers and selections in the form (text fields are shortened, and recognised paths, e-mail addresses, IBANs, long numbers and quoted text are replaced), a submission timestamp, the app version, OS platform and release, the configured OCR provider name and whether a license file is present. Where the app has no webhook address configured, nothing is sent. For bug reports the "include logs" box is ticked by default. The form shows the log lines before you send them, and the text it shows is the text that is sent. Only warning and error entries written by a build that scrubs log content are used, at most 20 entries and 2,900 characters. Every line goes through the strict scrubber described under crash reporting above: paths, e-mail addresses, IBANs, long numbers and quoted text are replaced, and so is every word that is not one of the app's own message words. The app writes log entries as fixed messages with numbers and codes, not as free text, and it does not write the message of an error from handling a document. That is a safeguard, not a guarantee: a document word that is also one of the app's own words can pass. Untick the box to send no log lines.
- Updates: the app checks GitHub Releases about 10 seconds after each launch and when you click "Check for updates", and downloads updates automatically. The requests carry the platform and a random ID that the update library creates once and stores in your user-data folder, and downloading an update can reveal which version you run. Every release build does this, including the portable Windows build, and there is no setting to turn it off. Packaged development builds have publishing disabled.
Each request that leaves your machine reveals your IP address, or that of your VPN or proxy, to the server that receives it.
The machine ID is the first 32 hexadecimal characters of a SHA-256 hash over your network adapters' hardware (MAC) addresses. If no adapter has a usable address, the app creates a random UUID, keeps it in your user-data folder and hashes that instead. It is a pseudonymous device fingerprint, not your name or e-mail address, and it is used to enforce trial limits and to bind a license to a device. The app checks the Ed25519 signature on the trial server's status and usage responses and ignores a response whose signature is missing or wrong.
What an AI Provider Receives from Ask AI
The Ask AI panel builds a prompt from a preset instruction and the masked text of the project's review, including documents other than the one open on screen. Multi-step chains also send every earlier answer from the model in each later step, without masking those answers again.
- Every span you did not reject (accepted or not yet reviewed) is masked. Where spans overlap, their combined range is masked and one placeholder stands for it.
- A span you rejected stays visible, unless another detected or manually added span that you did not reject covers it.
- Text the detectors did not find is sent unchanged unless a mask covers it. You can add such a mask yourself on the review screen before sending.
Detection is incomplete, and how much it finds varies by preset and by language. The app shows a risk assessment based on the detected and manually added review spans and your decisions on them. It treats every rejected span as visible, even when another mask covers it, and it cannot assess PII that is not among the review spans. When the assessed risk is high, sending through OpenRouter, copying the prompt and opening a ChatGPT link ask you to confirm first. Running the prompt through Ollama on this machine does not ask. High assessed risk alone does not stop you from sending once you confirm, but Ask AI is unavailable while detection is marked incomplete or unavailable, and requests remain subject to validation and the provider's requirements.
Placeholders are pseudonyms, not anonymisation. A placeholder such as <PRIVATE_PERSON_1> names the category of what was removed. Detected spans with the same category and the same normalised text share a placeholder, and some inflected forms share the placeholder of the form they were derived from. Fragments of merged spans and spans you add by hand get numbers of their own. Repeated placeholders therefore show the provider how the app grouped the mentions. That grouping is not reliable identity, but it still hints at how many distinct people, phone numbers or accounts the document contains. Everything around the placeholders (roles, dates, places, events and any detail not detected as PII) goes to the provider as written.
The ChatGPT button puts the whole prompt, including the masked text, into the link it opens in your web browser, or onto the clipboard when it is too long for a link; a link can stay in your browser history. The link between each placeholder and the original text stays on your device, and the Ask AI request does not include it. The app does not put original values back into a model's answer.
The built-in OCR scores every scanned page it reads. If a page is scored poor or unreadable, Ask AI and the export of the redacted text ask you to confirm that you have looked at it, and the main process refuses the export until you have; your confirmation applies to the text of that page and is dropped when the page is read again. The score is a safety net with limits: it does not cover pages read with Ollama (they have no confidence score), documents that needed no OCR, or projects created before the score existed, and a good score does not show that everything on the page was read, because text the page analysis dropped is not counted.
Local Processing
PII detection and masking run on your device, once the models are on it. The detectors are pattern and checksum recognizers ported from Microsoft Presidio, the nym multilingual classifier and the OPF person and address model, plus a Polish-only GLiNER2 model. The models run through ONNX Runtime. Detection never uses cloud inference.
Cloud Provider Data Flow
The original, unmasked document never goes to a cloud provider: text extraction and OCR run on your device (OCR with the built-in Tesseract engine or Ollama on this machine; there is no cloud OCR). The only content that can reach a cloud provider is masked text you choose to send with Ask AI, as described above. Requests to OpenRouter go directly from your device with the API key you entered; they do not pass through a Maskeno server. A local alternative exists: a local Ollama model instead of a cloud model.
These are the third-party services the app can contact. Your use of them is governed by their privacy policies:
- OpenAI Privacy Policy (ChatGPT link)
- OpenRouter Privacy Policy (Ask AI)
- Hugging Face Privacy Policy (fallback for the nym and OPF model downloads)
- Cloudflare Privacy Policy (nym, OPF and GLiNER2 Polish model downloads from models.audior.app)
- jsDelivr Privacy Policy (Tesseract OCR language-data downloads)
- GitHub General Privacy Statement (app updates)
- Slack Privacy Policy (in-app feedback delivery)
- Polar.sh Privacy Policy (licensing and payments)
- Sentry Privacy Policy (crash reporting, opt-in)
- Aptabase Privacy Policy (usage analytics, opt-in)
Data Stored on Your Device
Project database. Your projects, document text and review spans are stored in a local SQLite database in your user-data folder, together with its write-ahead log and up to three backups. The database is encrypted by SQLite3 Multiple Ciphers, whose default scheme (ChaCha20-Poly1305) applies; it is not in SQLCipher format. The key is a random 256-bit value created on first launch and stored in a file called db.key next to the database, protected with Electron's safeStorage: DPAPI on Windows (any program running under your Windows account can decrypt it), your Keychain on macOS, and a secret service such as GNOME Keyring or KWallet on Linux. If safeStorage reports that encryption is unavailable when the key is first created, Maskeno writes the key in plain text and only logs a warning. On Linux without a running secret service the key is either plain text or encrypted with a password built into Electron, which Electron's documentation describes as unprotected. In either case anyone who can read your user-data folder can open the database. For each scanned page that the built-in OCR read, the database also keeps a read-quality summary (a score, the mean confidence, word counts and the character ranges of doubtful words, but no text) and the time you confirmed the page.
Files that are not encrypted: rendered page images, copies of source files the app imported into its own folder, the app log, your settings file (the individual keys inside it are encrypted, the file is not), the license file's metadata, the machine-ID and update-ID files, the trial state file (readable text protected against tampering, not encrypted), SQLite's shared-memory index file, and, in your home folder, a crash log (for an uncaught error its class, its code and its stack positions inside the app, never its message, and the install ID), a startup-timing log, and a debug log when startup debugging is switched on.
API keys and the license key. API keys are kept in the settings file and the license key in license.json, encrypted with the same safeStorage mechanism. If safeStorage reports encryption unavailable, API keys are stored in plain text, and Settings shows a warning in that case, and the license key is only base64-encoded, which is not encryption. Normal requests send your stored key directly to the selected provider, without passing it through a Maskeno server. The sanitising of feedback and error reports is not a guarantee that every credential is removed from free text. The only API key Settings stores is your OpenRouter key. Keys that earlier versions stored for providers the app no longer uses (ElevenLabs, Resemble, Gemini, OpenAI and Anthropic) are deleted from the settings file once, the first time this version starts; if a deletion fails, it is retried at the next start.
Logs. The app log in your user-data folder is written from a fixed list of events: a fixed message plus values of known types (counts, sizes, durations, references such as file# and eight hexadecimal characters for a file name, constants of the app) and, for an error, its class, its code and its stack positions inside the app. The app is built not to write file names, paths, document text or, for operations that can handle a document, error messages; for settings, license, update and similar operations an error message can be written after strict scrubbing. Text that the app does not control passes through a scrubber before it is written. That is a design rule with tests behind it, not a guarantee. Entries that older versions of the app wrote can hold more, because those versions wrote free text; the bug-report form ignores them. The "Copy Diagnostics" button in Settings puts the app version, the system, the anonymous install ID and the recent log lines of every level, strictly scrubbed, on the clipboard; nothing is sent.
Downloaded model files and OCR language data. These are public downloads and hold no document content. The app does not encrypt them, and "Reset all data" does not remove them. The detection models are kept in the default cache folder of the transformers.js library, a .cache folder inside the library's own package folder among the application's files; the app sets no other location. The OCR language files (pol.traineddata, eng.traineddata and so on) are kept in a folder named maskeno-tessdata inside your operating system's temporary folder, which the system or a cleanup tool can empty. The app then downloads them again the next time it needs them, unless TESSDATA_PATH is set. When TESSDATA_PATH is set, the app also keeps copies of the files it read from that folder, in a subfolder of maskeno-tessdata whose name is src- followed by twelve hexadecimal characters.
Website Analytics
This website loads the Plausible Analytics script. See Plausible's data policy for what it collects.
Data Retention & Erasure
- Deleting a project removes its rows from the database and its page-image folder. Older copies of the project stay inside the encrypted backups until newer backups replace them. Files the app copied into its import folder are not removed.
- "Reset all data" in Settings deletes the database with its write-ahead and shared-memory files, the backups, the settings file (including stored API keys), voice samples and the license file. It leaves page images, imported files, logs,
db.key, the machine-ID and update-ID files, anyMaskeno.db.corrupted-*recovery copies with their sidecar files (these can still hold project data), the downloaded model files and OCR language data, the log files in your home folder, and the trial state file. The trial state file is kept outside the user-data folder on purpose, so that reinstalling does not reset the trial. Delete any of these yourself if you want them gone. - Trial server: it stores your machine ID, trial usage counts, OS platform and app version (sent at registration), the time the record was created and the time of the last registration or usage report. The server's code also uses your IP address as a rate-limit key in its key-value store, which expires 60 seconds after the last counted request. That statement covers the server's own code only, not logs that a hosting provider may keep. The app does not currently display the machine ID that identifies the record. To request erasure, email [email protected].
- License data is handled by Polar.sh under their privacy policy. For license records, contact [email protected].
Your Rights Under GDPR
As a data subject in the European Union, you have the rights of access, rectification, erasure, data portability, objection, and to withdraw consent. The data flows described above include the trial record, short-lived IP rate-limit entries, anything you send through the feedback form, crash and usage reports you opted into, website analytics, and licensing data held by Polar.sh. Contact us at [email protected].
Children's Privacy
Maskeno is designed for use by adults, including professionals handling sensitive documents. We do not knowingly collect data from children under 16.
Changes to This Policy
We may update this Privacy Policy from time to time. We will notify you of any changes by updating the "Last updated" date at the top of this page.
Contact Us
- Email: [email protected]
- GitHub: Open an issue