OpenWhispr is a dictation app. Press a global hotkey, talk, and the transcribed text is pasted wherever your cursor is, in any app. You choose the engine: local models such as Whisper and NVIDIA Parakeet that keep your audio on the device, or a cloud provider with your own key when you want speed. Around that core it has grown a voice assistant hotkey that sends what you say to GPT, Claude, Gemini or a local model, dictation translation, notes with semantic search, and meeting transcription with speaker labels.
It earns a place as an open-source answer to paid dictation tools like Wispr Flow. Everything core works offline with local models, GPU acceleration covers Metal, CUDA and Vulkan, and it runs on macOS, Windows and Linux. The project is active, with a release most weeks.
Know what you switch on. Meeting transcription records calls, so check that everyone is fine with that; cloud sync, team spaces and shared notes go through OpenWhispr's own service when you sign in; and the assistant can optionally send a screenshot of your screen as context.
- Repository: github.com/OpenWhispr/openwhispr
- Licence: MIT (MIT License)
- Language: JavaScript. Stars: 10K. Forks: 1,287. Last push: Oct 26, 2026.
- Scan: safe, Oct 26, 2026, commit c0639e5
Who it is for
Anyone who types a lot and would rather talk, people who need dictation that never leaves the laptop, and Linux users short on good dictation options.
Getting started
1. macOS: install with the community Homebrew cask (Windows .exe and Linux packages are on the releases page)
brew install --cask openwhispr2. Or run from source (needs Node.js 24+)
git clone https://github.com/OpenWhispr/openwhispr.git && cd openwhispr && npm install && npm run devSafety scan
We cloned OpenWhispr/openwhispr at commit c0639e5 on Oct 26, 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.
- All 19 secret hits are fake Slack tokens such as xoxp-secret-token in test files under test/helpers/connectors/, used to test the Slack connector.
- The six persistence hits are one feature: on Linux, pasting text uses ydotool, which needs a daemon. resources/linux/after-install.sh, run by the .deb or .rpm installer, adds a udev rule for /dev/uinput, adds you to the input group and enables a ydotoold user service. That is what lets it type into other apps, and it means any program in that group can simulate input.
- npm hooks: postinstall rebuilds native modules for Electron; the prepack and prebuild steps download whisper.cpp, llama-server, sherpa-onnx, yt-dlp, Qdrant and model files that end up inside the app.
- 24 known advisories across the desktop and mobile lockfiles, none critical: a clean result for an Electron app this size. The bare-IP hits are tests of endpoint validation using private and loopback addresses.
- 17 workflows, none using pull_request_target; only 1 of 21 third-party actions is pinned to a commit. Security policy, Dependabot and CodeQL all present.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | 19 candidates found and read; see the notes above. |
| Suspicious code | 6 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 3 npm lifecycle scripts |
| Committed binaries | None. |
| CI workflows | 17 workflows. None use pull_request_target. 20 of 21 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 40 distinct hosts referenced from source; most often github.com, api.example.com, api.openai.com, sandbox.dub.link. 36 URLs to a bare IP address, listed under the raw findings. |
| Known vulnerabilities | 24 advisories across 1,973 pinned packages: 0 critical, 8 high, 11 moderate, 5 low. openwhispr-mobile/package-lock.json: 1,387 packages, 5 advisories; package-lock.json: 991 packages, 19 advisories. |
| Project hygiene | Has security policy, automated dependency updates, CodeQL, licence file, contributing guide. |
| 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.
Secret candidates (19, redacted)
Pattern hits (6)
| Where | Rule | Match |
|---|---|---|
| resources/linux/after-install.sh:95 | persistence | su - "$REAL_USER" -c "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR systemctl --user enable $SERVICE_NAME" 2>/dev/null || true |
| src/components/SettingsPage.tsx:3706 | persistence | cmd: "systemctl --user daemon-reload && systemctl --user enable ydotoold", |
| src/components/SettingsPage.tsx:3732 | persistence | cmd: "systemctl --user enable ydotoold && systemctl --user start ydotoold", |
| src/components/SettingsPage.tsx:3736 | persistence | cmd: "systemctl --user enable --now ydotool.service", |
| src/helpers/clipboard.js:2241 | persistence | "\n\nNote: ydotool is installed but the ydotoold daemon is not running. Start it with: systemctl --user enable --now ydotoold"; |
| src/helpers/ensureYdotool.js:155 | persistence | "- No systemd service found for ydotoold. Enable it with:\n systemctl --user enable ydotoold && systemctl --user start ydotoold" |
URLs to bare IP addresses (36)
| Where | Rule | Match |
|---|---|---|
| openwhispr-mobile/modules/background-uploader/tests/ProviderRequestTransportTests.swift:183 | ip-literal-url | for value in ["https://example.com", "http://127.0.0.1", "http://192.168.1.2", "http://100.64.1.1", "http://host.local", "http://host.ts.net", "http://[::1]"] { |
| openwhispr-mobile/src/lib/__tests__/providerEndpoints.test.ts:6 | ip-literal-url | expect(isSecureHttpEndpoint('http://100.101.102.103/v1')).toBe(true); |
| test/helpers/dictationTranslationInference.test.js:221 | ip-literal-url | translationRemoteUrl: "http://192.0.2.1:8080", |
| test/helpers/enterpriseEndpointGuard.test.js:61 | ip-literal-url | accepts("https://8.8.8.8/openai"); |
| test/helpers/providerConnectionTest.test.js:108 | ip-literal-url | "http://100.101.102.103/v1", |
| test/helpers/secureEndpoint.test.js:60 | ip-literal-url | assert.equal(isSecureHttpEndpoint("http://172.15.0.1:8080"), false); |
| test/helpers/secureEndpoint.test.js:61 | ip-literal-url | assert.equal(isSecureHttpEndpoint("http://172.32.0.1:8080"), false); |
| test/helpers/secureEndpoint.test.js:67 | ip-literal-url | assert.equal(isSecureHttpEndpoint("http://100.64.0.1:8080"), true); |
| test/helpers/secureEndpoint.test.js:68 | ip-literal-url | assert.equal(isSecureHttpEndpoint("http://100.127.255.254:8080"), true); |
| test/helpers/secureEndpoint.test.js:69 | ip-literal-url | assert.equal(isSecureHttpEndpoint("http://100.63.0.1:8080"), false); |
| test/helpers/secureEndpoint.test.js:70 | ip-literal-url | assert.equal(isSecureHttpEndpoint("http://100.128.0.1:8080"), false); |
| test/helpers/urlAudioDownloader.test.js:523 | ip-literal-url | req.emit("redirect", 302, "GET", "https://93.184.216.34/real.mp3"); |
| test/helpers/urlAudioDownloader.test.js:537 | ip-literal-url | const result = await downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null); |
| test/helpers/urlAudioDownloader.test.js:541 | ip-literal-url | assert.equal(requests[0].url, "https://93.184.216.34/file.mp3"); |
| test/helpers/urlAudioDownloader.test.js:542 | ip-literal-url | assert.equal(requests[1].url, "https://93.184.216.34/real.mp3", "restart must target the redirect URL"); |
| test/helpers/urlAudioDownloader.test.js:569 | ip-literal-url | downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null), |
| test/helpers/urlAudioDownloader.test.js:586 | ip-literal-url | req.emit("redirect", 302, "GET", "http://93.184.216.34/x.mp3"); |
| test/helpers/urlAudioDownloader.test.js:595 | ip-literal-url | downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null), |
| test/helpers/urlAudioDownloader.test.js:612 | ip-literal-url | req.emit("redirect", 302, "GET", "https://93.184.216.34/next.mp3"); |
| test/helpers/urlAudioDownloader.test.js:621 | ip-literal-url | downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null), |
| test/helpers/urlAudioDownloader.test.js:651 | ip-literal-url | const promise = downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, ac.signal); |
| test/helpers/urlAudioDownloader.test.js:726 | ip-literal-url | const result = await downloader.download("https://93.184.216.34/interview.mp3", null, null); |
| test/helpers/urlAudioDownloader.test.js:744 | ip-literal-url | headers: { location: "http://93.184.216.34/x.mp3" }, |
| test/helpers/urlAudioDownloader.test.js:748 | ip-literal-url | await assert.rejects(downloader.download("https://93.184.216.34/file.mp3", null, null), { |
| and 12 more | ||
npm lifecycle scripts (3)
openwhispr-mobile/package.jsonpostinstall:patch-package --error-on-fail && npm run test:security && npm run download:brand-fontspackage.jsonpostinstall:electron-builder install-app-depspackage.jsonprepack:npm run compile:native && npm run download:whisper-cpp && npm run download:llama-server && npm run download:sherpa-onnx && npm run download:yt-dlp && npm run download:qdrant && npm run download:meetin…
Worst known vulnerabilities (24 of 24)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-mrhx-c7xc-m3w5 | high | zod@3.25.76 | Zod can exhaust memory while validating oversized constrained collections |
| GHSA-86w9-cpqp-85rv | high | node-forge@1.4.0 | node-forge RSA PKCS#1 v1.5 signature verification accepts extra nested DigestAlgorithm elements |
| GHSA-mrhx-c7xc-m3w5 | high | zod@4.4.1 | Zod can exhaust memory while validating oversized constrained collections |
| GHSA-xjj6-f33q-929c | high | undici@7.30.0 | undici vulnerable to Denial of Service via unhandled error on excessive content-encodings |
| GHSA-jr5w-w6x2-84p9 | high | electron@41.10.7 | Electron: Mitigation for compromised eMudhra intermediate certificates |
| GHSA-834v-8h66-3h79 | high | happy-dom@20.14.5 | Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution |
| GHSA-xjj6-f33q-929c | high | undici@6.29.0 | undici vulnerable to Denial of Service via unhandled error on excessive content-encodings |
| GHSA-mrhx-c7xc-m3w5 | high | zod@4.3.6 | Zod can exhaust memory while validating oversized constrained collections |
| GHSA-vcc3-ghjq-m6fr | moderate | decode-uri-component@0.2.2 | decode-uri-component: Denial of service via exponential decoding of malformed percent-encoded input |
| GHSA-rj75-hqrm-r3gf | moderate | postcss-selector-parser@6.1.4 | PostCSS: Quadratic complexity in flat selector parsing allows CPU exhaustion |
| GHSA-4qv8-2p9j-2685 | moderate | undici@7.30.0 | undici vulnerable to Denial of Service via unbounded memory in the EventSource parser |
| GHSA-f72v-6w45-q472 | moderate | undici@7.30.0 | undici vulnerable to Subresource Integrity bypass via non-lowercase algorithm tokens |
| GHSA-2p8c-gr87-2676 | moderate | electron@41.10.7 | Electron: SameSite cookie restrictions are not applied to windows opened from links |
| GHSA-997q-gvvc-jwmx | moderate | electron@41.10.7 | Electron: openExternal permission requests can be attributed to the wrong origin |
| GHSA-fw47-9c62-h3vw | moderate | electron@41.10.7 | Electron: HID and USB permission checks can be attributed to the wrong origin |
| GHSA-x562-297c-g43g | moderate | electron@41.10.7 | Electron: Pointer lock permission requests can be attributed to the wrong frame |
| GHSA-w74x-v6f9-j3xg | moderate | fast-uri@3.1.8 | fast-uri vulnerable to URI scheme spoofing via percent-encoded scheme decoding |
| GHSA-wjr9-j795-jxcr | moderate | fast-uri@3.1.8 | fast-uri vulnerable to host confusion via unchecked WHATWG host canonicalization |
| GHSA-4qv8-2p9j-2685 | moderate | undici@6.29.0 | undici vulnerable to Denial of Service via unbounded memory in the EventSource parser |
| GHSA-3qxv-5xgr-q7w9 | low | electron@41.10.7 | Electron: Frame subscription trusts GPU-supplied frame geometry |
| GHSA-98mx-cjp9-gchm | low | electron@41.10.7 | Electron: Extension tabs.query returns tab URLs and titles without the tabs permission |
| GHSA-h3v5-h239-pg29 | low | electron@41.10.7 | Electron: Off-screen rendering on macOS trusts GPU-supplied frame geometry |
| GHSA-333x-w4r6-6crp | low | fast-uri@3.1.8 | fast-uri vulnerable to scheme injection via an unescaped first-segment colon in suffix references |
| GHSA-xhq7-xmqj-769h | low | fast-uri@3.1.8 | fast-uri vulnerable to improper path equivalence in relative reference normalization |
By the numbers
| Stars | 10K |
|---|---|
| Forks | 1,287 |
| Contributors | 110 |
| Commits | 2,497 |
| Open issues | 261 |
| Open pull requests | 269 |
| Releases | 95 |
| Latest release | v1.10.4 |
| Licence | MIT |
| Main language | JavaScript |
| Project age | 1 year |
| Last push | Oct 26, 2026 |
| Tracked files | 3,185 |
| Lines of code | 679.1K |
| Checkout size | 37 MB |
Lines by language: JavaScript 322.9K, TypeScript 250.1K, JSON 62.8K, Swift 16.4K, Markdown 10.5K, C 6,818.
Questions
Is OpenWhispr free?
Yes. It is MIT-licensed and free for personal and commercial use. Local models cost nothing to run; cloud transcription and AI features use your own API keys, and signed-in features such as cloud sync run on OpenWhispr's service.
Does OpenWhispr send my voice to the cloud?
Only if you choose a cloud model. With a local model such as Whisper or Parakeet, transcription happens on your machine and the audio never leaves it.
Does OpenWhispr work on Linux?
Yes, as AppImage, .deb, .rpm and tar.gz builds. Automatic pasting relies on ydotool, which the .deb and .rpm installers configure for you.
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.
