Handy is push-to-talk dictation for the whole desktop. Hold a keyboard shortcut (or tap it to toggle), speak, release, and the transcription is pasted into whatever text field has focus: an email, a chat box, a code editor. Silero voice activity detection trims the silence, and transcription runs on your own computer with your choice of OpenAI's Whisper models, from Small to Large and GPU-accelerated where available, or NVIDIA's Parakeet V3, which is tuned for CPUs and detects the language automatically. No audio is sent anywhere.
It is the open-source pick in most lists of alternatives to paid dictation apps such as Wispr Flow, and it deliberately does one job, with the stated aim of being a simple base that others extend. Around that job sit a transcript history, a custom dictionary, command-line flags for controlling a running instance (which is how Wayland users bind it to a desktop shortcut) and a community Raycast extension.
Handy is written by CJ Pais in Rust with Tauri, MIT-licensed, at about 32,700 stars and version 0.9.8, with signed builds for Windows, macOS on Apple Silicon and Intel, and Linux.
- Repository: github.com/cjpais/Handy
- Licence: MIT (MIT License)
- Language: Rust. Stars: 32.7K. Forks: 3,027. Last push: Oct 3, 2026.
- Scan: safe, Oct 3, 2026, commit 8605699
Who it is for
Anyone who would rather talk than type, people with RSI or other accessibility needs, and privacy-minded users who want dictation that works on a plane and never uploads a recording.
Getting started
1. macOS, with Homebrew (the cask is community-maintained)
brew install --cask handy2. Windows, with winget (also community-maintained; installers are on the releases page)
winget install cjpais.Handy3. Debian or Ubuntu: download the .deb from the releases page and install it with APT
sudo apt install ./Handy_*.debOn first launch Handy asks for microphone and accessibility permissions, the second so it can paste into other apps, and you set your shortcut in Settings. On Linux it needs xdotool (X11) or wtype or dotool (Wayland) to type text, and on Wayland the global shortcut is set in your desktop environment to run handy --toggle-transcription.
Safety scan
We cloned cjpais/Handy at commit 8605699 on Oct 3, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.
- No secrets and no bare-IP URLs across 387 files and about 72,500 lines of Rust and TypeScript. The two pattern hits are in src-tauri/src/autostart.rs: a comment and a test path for the macOS LaunchAgent that the Launch at login setting writes. That setting is off by default.
- The npm postinstall hook runs scripts/check-nix-deps.ts, a developer check that the Nix build's dependency hashes are current; the one Cargo build script is Tauri's standard one. Neither runs when you install the app.
- We read the network code. Models download from blob.handy.computer, update checks are on by default and updates are verified against the Tauri signing key in tauri.conf.json, and the only other hosts are the LLM providers (OpenAI, OpenRouter, Anthropic, Groq, DeepSeek, Z.AI and others) for the optional post-processing step, which is off by default and sends transcript text, not audio, to the provider you choose. No telemetry.
- 38 known advisories (8 high, none critical) in src-tauri/Cargo.lock's 852 crates, in openssl, quick-xml and rustls-webpki, which arrive through Tauri and its HTTP stack. The traffic they could matter for is model downloads and update checks.
- Nine workflows, none using pull_request_target; 1 of 12 third-party actions is pinned to a commit. Licence and contributing guide present; no security policy, Dependabot or CodeQL.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | None found. |
| Suspicious code | 2 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 1 npm lifecycle script. 1 Cargo build script |
| Committed binaries | None. |
| CI workflows | 9 workflows. None use pull_request_target. 11 of 12 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 14 distinct hosts referenced from source; most often blob.handy.computer, github.com, openrouter.ai, api.deepseek.com. No URLs to bare IP addresses. |
| Known vulnerabilities | 38 advisories across 852 pinned packages: 0 critical, 8 high, 9 moderate, 5 low, 16 unrated. src-tauri/Cargo.lock: 852 packages, 38 advisories. |
| Project hygiene | Has licence file, contributing guide. Missing security policy, automated dependency updates, CodeQL. |
| OpenSSF Scorecard | Not scored: the project is not in Scorecard's weekly index. |
The raw findings
Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.
Pattern hits (2)
| Where | Rule | Match |
|---|---|---|
| src-tauri/src/autostart.rs:102 | persistence | /// `~/Library/LaunchAgents/{app name}.plist`. |
| src-tauri/src/autostart.rs:135 | persistence | Path::new("/Users/someone/Library/LaunchAgents/Handy.plist") |
npm lifecycle scripts (1)
package.jsonpostinstall:bun scripts/check-nix-deps.ts
Worst known vulnerabilities (24 of 38)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-8c75-8mhr-p7r9 | high | openssl@0.10.75 | rust-openssl has incorrect bounds assertion in aes key wrap |
| GHSA-ghm9-cr32-g9qj | high | openssl@0.10.75 | rust-openssl: rustMdCtxRef::digest_final() writes past caller buffer with no length check |
| GHSA-hppc-g8h3-xhp3 | high | openssl@0.10.75 | rust-openssl: Unchecked callback length in PSK/cookie trampolines leaks adjacent memory to peer |
| GHSA-pqf5-4pqq-29f5 | high | openssl@0.10.75 | rust-openssl: Deriver::derive and PkeyCtxRef::derive can overflow short buffers on OpenSSL 1.1.1 |
| GHSA-xp3w-r5p5-63rr | high | openssl@0.10.75 | rust-openssl has undefined behavior in X509Ref::ocsp_responders for certificates with non-UTF-8 OCSP URLs |
| RUSTSEC-2026-0194 | high | quick-xml@0.38.4 | Quadratic run time when checking a start tag for duplicate attribute names |
| RUSTSEC-2026-0195 | high | quick-xml@0.38.4 | Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service |
| GHSA-82j2-j2ch-gfr8 | high | rustls-webpki@0.103.9 | rustls-webpki: Denial of service via panic on malformed CRL BIT STRING |
| GHSA-wrw7-89jp-8q8g | moderate | glib@0.18.5 | Unsoundness in `Iterator` and `DoubleEndedIterator` impls for `glib::VariantStrIter` |
| GHSA-phqj-4mhp-q6mq | moderate | openssl@0.10.75 | rust-openssl: Potential out-of-bounds write in `CipherCtxRef::cipher_update_inplace` for AES-KW-PAD ciphers |
| GHSA-xv59-967r-8726 | moderate | openssl@0.10.75 | rust-openssl vulnerable to heap buffer overflow when encrypting with AES key-wrap-with-padding |
| GHSA-2mjx-qc3c-rqvc | moderate | rustls@0.23.36 | TLS 1.3 handshake messages incorrectly accepted across encryption level boundaries |
| GHSA-pwjx-qhcg-rvj4 | moderate | rustls-webpki@0.103.9 | webpki: CRLs not considered authoritative by Distribution Point due to faulty matching logic |
| GHSA-7gcf-g7xr-8hxj | moderate | serde_with@3.16.1 | serde_with: KeyValueMap serialization panics on empty sequence or map entries |
| GHSA-3pv8-6f4r-ffg2 | moderate | tar@0.4.44 | tar has a PAX header desynchronization issue |
| GHSA-gchp-q4r4-x4ff | moderate | tar@0.4.44 | tar-rs incorrectly ignores PAX size headers if header size is nonzero |
| GHSA-j4xf-2g29-59ph | moderate | tar@0.4.44 | tar-rs `unpack_in` can chmod arbitrary directories by following symlinks |
| GHSA-xmgf-hq76-4vx2 | low | openssl@0.10.75 | rust-opennssl has an Out-of-bounds read in PEM password callback when returning an oversized length |
| GHSA-cq8v-f236-94qc | low | rand@0.7.3 | Rand is unsound with a custom logger using rand::rng() |
| GHSA-cq8v-f236-94qc | low | rand@0.8.5 | Rand is unsound with a custom logger using rand::rng() |
| GHSA-965h-392x-2mh5 | low | rustls-webpki@0.103.9 | webpki: Name constraints for URI names were incorrectly accepted |
| GHSA-xgp8-3hg3-c2mh | low | rustls-webpki@0.103.9 | webpki: Name constraints were accepted for certificates asserting a wildcard name |
| RUSTSEC-2026-0190 | unknown | anyhow@1.0.102 | Unsoundness in `Error::downcast_mut()` |
| RUSTSEC-2025-0141 | unknown | bincode@2.0.1 | Bincode is unmaintained |
By the numbers
| Stars | 32.7K |
|---|---|
| Forks | 3,027 |
| Contributors | 173 |
| Commits | 872 |
| Open issues | 87 |
| Open pull requests | 98 |
| Releases | 66 |
| Latest release | v0.9.8 |
| Licence | MIT |
| Main language | Rust |
| Project age | 1 year |
| Last push | Oct 3, 2026 |
| Tracked files | 387 |
| Lines of code | 72.5K |
| Checkout size | 8 MB |
Lines by language: Rust 29.9K, JSON 21.4K, TypeScript 16.2K, Markdown 1,739, YAML 1,452, CSS 658.
Questions
Is Handy free?
Yes. Handy is MIT-licensed and free on Windows, macOS and Linux, with no account, subscription or usage limit. The project is supported by sponsors rather than a paid tier.
Does Handy work offline?
Yes. Once a model is downloaded, recording, voice detection and transcription all run on your computer and no audio goes to the cloud. A connection is only needed for models and update checks, and for the optional post-processing step, off by default, which sends transcript text to an LLM provider you choose.
Which model should I pick in Handy?
Parakeet V3 is the README's choice for machines without a strong GPU: it is optimized for CPUs and detects the language automatically. Whisper models (Small, Medium, Turbo and Large) use the GPU when one is available; the larger ones are more accurate but slower and need more memory.
This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.
