# Privacy & Security This page answers one question: **what can VoxCtrl actually see, and what does installing it change about your machine?** Everything below describes what the code does, with pointers to where it does it, so you can check rather than take our word for it. --- ## The short version | Question | Answer | |---|---| | Can VoxCtrl read what I type in other applications? | **No.** Your desktop delivers shortcuts; VoxCtrl is never given keystrokes. | | Does installing it grant any process new access to my keyboard? | **No.** No udev rule, no `input` group, nothing. | | Does my audio leave the machine? | **No by default.** Audio only leaves your machine if you explicitly configure the Remote Speech Engine pointing to an external or LAN server. | | Does it send telemetry or analytics? | **No.** Nothing about you or your machine is transmitted unless you file a bug report and press Send — see [Bug reports](#bug-reports). | | Does it phone home at all? | **Only when you press "Check for updates."** VoxCtrl never checks on its own; that request carries no identifiers, nothing about you. [Details](#network). | | Can I send you diagnostics when something breaks? | **Yes, if you choose to.** Settings → Bug Report shows you the whole report before it goes anywhere. [Details](#bug-reports). | | Does it need root? | **No.** Administrator rights are requested once, optionally, to install packages. | | Can I verify all this? | Yes — see [Verifying it yourself](#verifying-it-yourself). | --- ## Keystrokes VoxCtrl needs to know when you press your dictation shortcut. There are two ways an app can learn that on Linux, and they are not equivalent. ### What VoxCtrl does: ask the desktop VoxCtrl identifies itself to the portal as `ai.voxctrl.app` and registers its shortcuts with `org.freedesktop.portal.GlobalShortcuts`. Your desktop compositor grabs the keys and sends VoxCtrl a D-Bus signal — `Activated` or `Deactivated`, carrying a shortcut ID — when one fires. That signal is the entire input. VoxCtrl does not receive, and cannot request, anything about keys it did not register. There is no filtering step to trust and no policy to get wrong: the data never arrives. The internal event type says the same thing (`crates/voxctrl-hotkeys/src/gestures.rs`): ```rust pub struct GestureEvent { pub binding_id: String, pub binding_label: String, pub target_id: String, pub kind: GestureKind, // Start | Stop } ``` No key names. No timing of individual presses. Nothing about what you typed. This is the only hotkey data that reaches the rest of the app, and the only thing the UI layer ever sees. On KDE Plasma, the portal accepts these shortcuts but leaves them disabled in System Settings until you tick a box and press Apply — an upstream KDE bug, not a VoxCtrl privacy question. See [KDE registers shortcuts disabled by default](hotkeys.md#kde-registers-shortcuts-disabled-by-default). ### What VoxCtrl deliberately stopped doing: reading `/dev/input` Earlier versions read `/dev/input/event*` directly, which requires a udev rule tagging input devices with `uaccess`: ``` SUBSYSTEM=="input", KERNEL=="event*", TAG+="uaccess" ``` VoxCtrl's installer wrote that rule. It is not narrow. It grants **every process running as you** the ability to read **every keystroke on the system** — your sudo password, your browser, your password manager — for as long as the rule exists. Not just VoxCtrl: a compromised npm postinstall script, any Electron app, any shell one-liner. systemd's own defaults (`/usr/lib/udev/rules.d/70-uaccess.rules`) grant `uaccess` on input devices to joysticks and nothing else, precisely to prevent this. VoxCtrl was overriding a deliberate security decision, on the user's behalf, during a first-run wizard. **It no longer does.** VoxCtrl never writes that rule, never runs `usermod -aG input`, and offers no button that does either. If your desktop has no shortcuts portal, VoxCtrl says so at launch and explains the trade-off rather than quietly widening access. Two tests fail if this regresses: - `the_privileged_script_never_touches_input_permissions` (`src-tauri/src/installer.rs`) - `never offers to grant keyboard access` (`tests/svelte/SetupWindow.test.ts`) ### The fallbacks, and being honest about them If your desktop provides no portal, VoxCtrl reads keys itself: X11 raw key events (XInput2) where there is an X server, and `/dev/input/event*` only where your system already lets this process do so. In either mode every keystroke does pass through the process. The X11 backend needs no permission at all — any X client may ask the server for raw key events — so it is what a stock Cinnamon, MATE or Xfce desktop uses. That makes it *easier* to reach than the evdev fallback, not more private: it sees exactly as much. It is chosen over evdev because it asks the user for nothing, and it is ranked below the portal for precisely this reason. VoxCtrl does not hide this. The Hotkeys tab and the setup window both say so in plain language, and `is_private` is false throughout the status API. What it does *not* do in that mode: - log key names (the reader has no logging on the key path) - store them (key names are transient `String`s and a small set of held keys) - send them anywhere (they never cross into the UI layer, and no network path can reach them) - read mice, touchpads or tablets (only devices that look like keyboards are opened; on X11 only key events are selected) - read its own injected keystrokes (synthetic devices — `uinput`, `XTEST`, anything named "virtual" — are always skipped, by name on evdev and by `sourceid` on X11) If you would rather it never happened at all, a desktop that implements the portal avoids it entirely — as does the native Cinnamon/MATE shortcut route, where the desktop holds the grab and VoxCtrl reads nothing. See [Hotkeys](hotkeys.md#linux--x11-raw-key-events-xinput2). ### Sending keys: the paste shortcut Receiving keys and *sending* them are different things, and this section is about sending. To paste a dictation VoxCtrl presses a shortcut (Ctrl+V) in the focused window, and while doing so it handles your clipboard: - **The clipboard backup** holds everything you had copied — passwords from a password manager included — in memory, for about a second, so it can be put back. It is never written to disk, logged, or sent anywhere. The dictation itself is marked so clipboard managers and cloud clipboard history skip it (`x-kde-passwordManagerHint`, `ExcludeClipboardContentFromMonitorProcessing`, `CanIncludeInClipboardHistory`). - **Wayland (GNOME, KDE)** has no way for a program to press keys on its own. VoxCtrl uses the desktop's `RemoteDesktop` portal, which shows **your desktop's own permission dialog** on first use. The grant lets VoxCtrl *send* key events; it does **not** let it read your keyboard (the portal has no such call — VoxCtrl never receives key events from it). The grant is remembered in `~/.config/voxctrl/portal-keyboard-token` and can be revoked in your desktop's settings. Declining it costs only the ability to paste into native Wayland windows. - **Live transcript text** (an optional feature of some custom overlays) puts the words you are saying on screen while you speak, which is visible in screen shares and recordings. It runs only when the active overlay opts in; see [Overlay UI](./overlays.md#live-transcript-text). See [Pasting Dictation](./paste.md). ### Windows Windows offers no portal equivalent; a low-level keyboard hook (`WH_KEYBOARD_LL`) is the only mechanism for application-defined global shortcuts. That hook sees all keystrokes. The same handling applies: nothing logged, nothing stored, nothing transmitted. `is_private` is false on Windows, and the Hotkeys tab says which keys pass through VoxCtrl rather than showing the padlock it shows for the portal. That was not always true: until v0.5.0 the status API grouped the Windows hook with the portal, so the UI would have claimed VoxCtrl did not read the keyboard while the hook read all of it. No release shipped a Windows build in that state — the Windows job was disabled in the release matrix — but the claim was in the code, and it is worth being explicit that it is gone. Two further properties follow from how the hook works rather than from anything VoxCtrl chose: - It never sees the **secure desktop** — the UAC prompt, the lock screen, Ctrl+Alt+Del. Nothing typed there reaches VoxCtrl, and shortcuts do not fire there either. - It does not receive keys destined for a **more-privileged process**. If you run something elevated, VoxCtrl sees nothing you type into it (and cannot dictate into it either). VoxCtrl also ignores its own synthesised keystrokes: every event it generates carries a marker in `dwExtraInfo`, and the hook skips those, so dictated text is never re-read as input. --- ## Audio - The microphone is opened when a gesture starts recording and closed when it stops. It is not held open in between. - For all local backends (`whisper.cpp`, `Moonshine`, `Parakeet TDT`), audio is transcribed 100% on your machine. No audio is ever uploaded anywhere. - **Remote Speech Engine ("Bring Your Own Voice Engine")**: If you explicitly configure `backend = "remote-openai"`, captured audio is encoded as a 16 kHz 16-bit mono WAV payload and transmitted via HTTP POST multipart request solely to the destination endpoint URL you configure (such as a server on your local network or a cloud API). The other destination for network traffic is one you configure: `http`, `webhook`, `chat` and `mcp` targets send **transcribed text** to wherever you point them, and LLM post-processing sends text to the endpoint you configure. Those are opt-in, per-target, and visible in `targets.toml`. Audio itself is never sent by any target. --- ## Network VoxCtrl has no telemetry, no analytics, and no automatic crash reporting. Nothing is ever sent about what you dictate, what you type, what you have installed, or who you are. Every request it makes happens because you asked for it — including the update check: | Trigger | Destination | Sends | |---|---|---| | Pressing "Check for updates" in Settings → General | `api.github.com/repos/JRufer/VoxCtrl/releases/latest`, plus `.../releases?per_page=30` when a newer version exists (for the change log) | A `User-Agent` of `VoxCtrl/`. Nothing else. | | Installing an offered update | `github.com` release download (the file and its `.minisig` signature) | Nothing beyond the request for the files | | Downloading a speech model | HuggingFace / the model host, on demand | Nothing beyond the request for the file | | Downloading a TTS voice | HuggingFace / the Piper voice host, on demand | Nothing beyond the request for the file | | Remote Speech Engine transcription (`backend = "remote-openai"`) | The speech-to-text endpoint you configured (LAN server or API) | 16 kHz mono WAV audio recorded during the gesture | | LLM post-processing | The OpenAI-compatible endpoint you configured | The transcribed text | | `http` / `webhook` / `chat` / `mcp` targets | The destination you configured | The transcribed text | | Pressing **Send report** in Settings → Bug Report | The bug-report relay, or GitHub if you choose that route | The report you were shown first — see [Bug reports](#bug-reports) | ### The update check, in full VoxCtrl never checks for updates on its own — there is no launch-time check and nothing to turn off. Pressing "Check for updates" in Settings → General sends a plain unauthenticated `GET` for the public release listing — the same URL anyone can open in a browser. When a newer version exists, one more `GET` for the recent release list fetches the change log for every version between yours and the latest. There is no request body, no cookie, no account, no install ID, and no way for it to carry one: GitHub is told which version of VoxCtrl is asking (because the API requires a `User-Agent`) and nothing more. What comes back is the release's tag, notes and file list, which is what the update window shows you. Nothing is downloaded or installed unless you press **Update and restart**. A downloaded update is then checked against the SHA-256 checksum GitHub publishes for it and — in a release build — against the release signature made by VoxCtrl's release workflow, before it replaces anything. A file that is not signed with VoxCtrl's key is never installed. See [release signing](release-signing.md). On a distro-packaged install (anything under `/usr`, `/opt` or `/nix/store`), "Update and restart" is unavailable — VoxCtrl detects that its files belong to a package manager and points you at it instead of overwriting them. See `InstallKind::ManagedPackage` in `crates/voxctrl-update/src/install.rs`. Once the app and its models are on disk, VoxCtrl runs fully air-gapped. **The code:** `crates/voxctrl-update/` is the whole of it — about 400 lines, with no dependency on the rest of the app. `release.rs` builds the (at most two) requests, `apply.rs` downloads and checks the digest, `signature.rs` checks the release signature, `src-tauri/src/updater.rs` is the manual check command and what it shows. --- ## Bug reports This is the one place VoxCtrl sends anything about your machine, and it is worth being exact about the conditions: **you open the page, you type the report, you read the whole thing, and you press a button.** There is no background reporting, no crash uploader, and nothing that fires on its own. What travels is a fixed, written-down list — version and build, OS, CPU, memory, display adapter, desktop and session, your settings with every secret and every path and every piece of free text stripped out, the shape of your output targets, and the tail of the log. What never travels is anything you dictated, any API key, any file path, your account name, your hostname, your custom vocabulary, your prompts, or your target labels. Two properties are worth singling out, because they are what make the promise checkable rather than aspirational: - **The preview is the report.** The text in "Show me exactly what will be sent" is the exact body that gets filed. There is no fuller version. - **Redaction is an allowlist.** Every text-bearing setting is classified by hand, and an unclassified one is omitted rather than guessed at. A test walks the real config struct and fails the build naming anything nobody has classified — so a setting added next year cannot leak by being forgotten. Full detail, including the exact list, the four ways to send a report, and how to check all of it yourself: **[docs/bug_reports.md](bug_reports.md)**. **The code:** `crates/voxctrl-bugreport/` — `redact.rs` for the settings rules, `scrub.rs` for paths and account names, `sysinfo.rs` for the machine facts, `logs.rs` for the log excerpt, `throttle.rs` for the limits. The test that enforces the allowlist is `tests/config_coverage.rs`. --- ## What the installer touches The optional setup step (`--install`, or the button in the setup window) does exactly three things: 1. Installs host packages via your package manager — WebKitGTK, OpenSSL, PortAudio, `wtype`, `xdotool`, clipboard helpers. These are what type transcriptions into your focused window. 2. Writes `~/.local/share/applications/ai.voxctrl.app.desktop` and an icon (the app does this itself on every launch too, without privileges — the installer is not needed for it). 3. **Removes** the udev rule older VoxCtrl versions installed, if it finds one. It does not create system users, services, or permissions. `scripts/uninstall.sh` reverses everything, including leftovers from older versions. --- ## Verifying it yourself **Confirm no input devices are open.** With VoxCtrl running on a desktop with portal support: ```bash ls -l /proc/$(pgrep -f voxctrl | head -1)/fd | grep /dev/input ``` No output means VoxCtrl has no input device open — it is not reading your keyboard. **Confirm which backend is live.** Settings → Hotkeys states it at the top of the page, and the setup window's first step says the same. A 🔒 means the portal path. **Confirm no udev rule was installed:** ```bash ls /etc/udev/rules.d/ | grep -i voxctrl # expect no output groups | grep input # expect no match, unless you added it yourself ``` **Watch the D-Bus traffic.** Everything VoxCtrl learns about your keyboard travels over this, so you can see the whole of it: ```bash dbus-monitor "interface='org.freedesktop.portal.GlobalShortcuts'" ``` Press your shortcut. You will see `Activated` and `Deactivated` with a shortcut ID. Type anything else, anywhere else: nothing appears. **Confirm no network traffic.** Run VoxCtrl with the network namespace cut off, and dictation still works once models are downloaded: ```bash sudo unshare -n sudo -u "$USER" ./VoxCtrl-x86_64.AppImage ``` **See the update check for yourself.** Start a capture, then press "Check for updates" in Settings → General: ```bash sudo tcpdump -n -i any 'host api.github.com' # one TLS connection, then nothing ``` Leave VoxCtrl running without pressing it and the same command stays silent — nothing triggers this request on its own. **Read the code.** The hotkey crate is about 1,500 lines and self-contained: - `crates/voxctrl-hotkeys/src/portal.rs` — the portal backend, the whole data path - `crates/voxctrl-hotkeys/src/trigger.rs` — what VoxCtrl is allowed to ask a desktop to bind - `crates/voxctrl-hotkeys/src/gestures.rs` — gesture recognition, which never sees a key name - `crates/voxctrl-hotkeys/src/linux.rs` — the backend order and why each is where it is - `crates/voxctrl-hotkeys/src/x11.rs` — the X11 backend, what it sees and what it filters - `src-tauri/src/installer.rs` — everything the installer does, in one file --- ## Reporting a problem If you find something here that is not true, that is a bug and we want to know. Open an issue at , or use Settings → Bug Report, which needs no GitHub account. For anything you would rather not disclose publicly, say so in the issue and we will arrange a private channel.