-
Notifications
You must be signed in to change notification settings - Fork 3
privacy
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.
| 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. |
| 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. |
| 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. |
| Does it need root? | No. Administrator rights are requested once, optionally, to install packages. |
| Can I verify all this? | Yes — see Verifying it yourself. |
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.
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):
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.
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)
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
Strings 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 bysourceidon 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.
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.
- 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.
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 |
A User-Agent of VoxCtrl/<version>. 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 |
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. 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.
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 one request,
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.
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.
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.
The optional setup step (--install, or the button in the setup window) does exactly three things:
- Installs host packages via your package manager — WebKitGTK, OpenSSL, PortAudio,
wtype,xdotool, clipboard helpers. These are what type transcriptions into your focused window. - Writes
~/.local/share/applications/ai.voxctrl.app.desktopand an icon (the app does this itself on every launch too, without privileges — the installer is not needed for it). - 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.
Confirm no input devices are open. With VoxCtrl running on a desktop with portal support:
ls -l /proc/$(pgrep -f voxctrl | head -1)/fd | grep /dev/inputNo 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:
ls /etc/udev/rules.d/ | grep -i voxctrl # expect no output
groups | grep input # expect no match, unless you added it yourselfWatch the D-Bus traffic. Everything VoxCtrl learns about your keyboard travels over this, so you can see the whole of it:
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:
sudo unshare -n sudo -u "$USER" ./VoxCtrl-x86_64.AppImageSee the update check for yourself. Start a capture, then press "Check for updates" in Settings → General:
sudo tcpdump -n -i any 'host api.github.com' # one TLS connection, then nothingLeave 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
If you find something here that is not true, that is a bug and we want to know. Open an issue at https://github.com/JRufer/VoxCtrl/issues, 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.