6 min read

OpenWhispr: Private Voice Dictation for Any App (GitHub, Scanned)

Press a hotkey, speak, and your words appear at the cursor, with local or cloud speech models.

OpenWhispr logo
✅
Scan: safe. Nothing malicious. On Linux the package sets up a udev rule and a background ydotool service so it can type for you, and the optional cloud features send data to the providers you choose. Scanned Oct 26, 2026; the full report is below.

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.

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 openwhispr

2. Or run from source (needs Node.js 24+)

git clone https://github.com/OpenWhispr/openwhispr.git && cd openwhispr && npm install && npm run dev

Safety 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

CheckResult
Secrets19 candidates found and read; see the notes above.
Suspicious code6 pattern hits found and read; every one is listed under the raw findings.
Install-time code3 npm lifecycle scripts
Committed binariesNone.
CI workflows17 workflows. None use pull_request_target. 20 of 21 third-party actions pinned to a tag rather than a commit.
Network hosts40 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 vulnerabilities24 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 hygieneHas security policy, automated dependency updates, CodeQL, licence file, contributing guide.
OpenSSF ScorecardNot 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)
WhereRuleMatch
test/helpers/connectors/credentialStore.test.js:28slack-tokenxoxp-s…ken (17 chars)
test/helpers/connectors/credentialStore.test.js:30slack-tokenxoxp-s…ken (17 chars)
test/helpers/connectors/credentialStore.test.js:32slack-tokenxoxp-s…ken (17 chars)
test/helpers/connectors/slackAuth.test.js:89slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackAuth.test.js:91slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackAuth.test.js:146slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackAuth.test.js:182slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackAuth.test.js:183slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackAuth.test.js:192slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackAuth.test.js:245slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackAuth.test.js:296slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackConnector.test.js:215slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackConnector.test.js:217slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackConnector.test.js:267slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackConnector.test.js:322slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackConnector.test.js:340slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackFixtures.js:13slack-tokenxoxp-1…ess (18 chars)
test/helpers/connectors/slackFixtures.js:28slack-tokenxoxp-1…s-2 (20 chars)
test/helpers/connectors/slackFixtures.js:145slack-tokenxoxp-1…ess (18 chars)
Pattern hits (6)
WhereRuleMatch
resources/linux/after-install.sh:95persistencesu - "$REAL_USER" -c "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR systemctl --user enable $SERVICE_NAME" 2>/dev/null || true
src/components/SettingsPage.tsx:3706persistencecmd: "systemctl --user daemon-reload && systemctl --user enable ydotoold",
src/components/SettingsPage.tsx:3732persistencecmd: "systemctl --user enable ydotoold && systemctl --user start ydotoold",
src/components/SettingsPage.tsx:3736persistencecmd: "systemctl --user enable --now ydotool.service",
src/helpers/clipboard.js:2241persistence"\n\nNote: ydotool is installed but the ydotoold daemon is not running. Start it with: systemctl --user enable --now ydotoold";
src/helpers/ensureYdotool.js:155persistence"- No systemd service found for ydotoold. Enable it with:\n systemctl --user enable ydotoold && systemctl --user start ydotoold"
URLs to bare IP addresses (36)
WhereRuleMatch
openwhispr-mobile/modules/background-uploader/tests/ProviderRequestTransportTests.swift:183ip-literal-urlfor 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:6ip-literal-urlexpect(isSecureHttpEndpoint('http://100.101.102.103/v1')).toBe(true);
test/helpers/dictationTranslationInference.test.js:221ip-literal-urltranslationRemoteUrl: "http://192.0.2.1:8080",
test/helpers/enterpriseEndpointGuard.test.js:61ip-literal-urlaccepts("https://8.8.8.8/openai");
test/helpers/providerConnectionTest.test.js:108ip-literal-url"http://100.101.102.103/v1",
test/helpers/secureEndpoint.test.js:60ip-literal-urlassert.equal(isSecureHttpEndpoint("http://172.15.0.1:8080"), false);
test/helpers/secureEndpoint.test.js:61ip-literal-urlassert.equal(isSecureHttpEndpoint("http://172.32.0.1:8080"), false);
test/helpers/secureEndpoint.test.js:67ip-literal-urlassert.equal(isSecureHttpEndpoint("http://100.64.0.1:8080"), true);
test/helpers/secureEndpoint.test.js:68ip-literal-urlassert.equal(isSecureHttpEndpoint("http://100.127.255.254:8080"), true);
test/helpers/secureEndpoint.test.js:69ip-literal-urlassert.equal(isSecureHttpEndpoint("http://100.63.0.1:8080"), false);
test/helpers/secureEndpoint.test.js:70ip-literal-urlassert.equal(isSecureHttpEndpoint("http://100.128.0.1:8080"), false);
test/helpers/urlAudioDownloader.test.js:523ip-literal-urlreq.emit("redirect", 302, "GET", "https://93.184.216.34/real.mp3");
test/helpers/urlAudioDownloader.test.js:537ip-literal-urlconst result = await downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null);
test/helpers/urlAudioDownloader.test.js:541ip-literal-urlassert.equal(requests[0].url, "https://93.184.216.34/file.mp3");
test/helpers/urlAudioDownloader.test.js:542ip-literal-urlassert.equal(requests[1].url, "https://93.184.216.34/real.mp3", "restart must target the redirect URL");
test/helpers/urlAudioDownloader.test.js:569ip-literal-urldownloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null),
test/helpers/urlAudioDownloader.test.js:586ip-literal-urlreq.emit("redirect", 302, "GET", "http://93.184.216.34/x.mp3");
test/helpers/urlAudioDownloader.test.js:595ip-literal-urldownloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null),
test/helpers/urlAudioDownloader.test.js:612ip-literal-urlreq.emit("redirect", 302, "GET", "https://93.184.216.34/next.mp3");
test/helpers/urlAudioDownloader.test.js:621ip-literal-urldownloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, null),
test/helpers/urlAudioDownloader.test.js:651ip-literal-urlconst promise = downloader.downloadViaProxy("https://93.184.216.34/file.mp3", null, ac.signal);
test/helpers/urlAudioDownloader.test.js:726ip-literal-urlconst result = await downloader.download("https://93.184.216.34/interview.mp3", null, null);
test/helpers/urlAudioDownloader.test.js:744ip-literal-urlheaders: { location: "http://93.184.216.34/x.mp3" },
test/helpers/urlAudioDownloader.test.js:748ip-literal-urlawait assert.rejects(downloader.download("https://93.184.216.34/file.mp3", null, null), {
and 12 more
npm lifecycle scripts (3)
  • openwhispr-mobile/package.json postinstall: patch-package --error-on-fail && npm run test:security && npm run download:brand-fonts
  • package.json postinstall: electron-builder install-app-deps
  • package.json prepack: 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)
AdvisorySeverityPackageSummary
GHSA-mrhx-c7xc-m3w5highzod@3.25.76Zod can exhaust memory while validating oversized constrained collections
GHSA-86w9-cpqp-85rvhighnode-forge@1.4.0node-forge RSA PKCS#1 v1.5 signature verification accepts extra nested DigestAlgorithm elements
GHSA-mrhx-c7xc-m3w5highzod@4.4.1Zod can exhaust memory while validating oversized constrained collections
GHSA-xjj6-f33q-929chighundici@7.30.0undici vulnerable to Denial of Service via unhandled error on excessive content-encodings
GHSA-jr5w-w6x2-84p9highelectron@41.10.7Electron: Mitigation for compromised eMudhra intermediate certificates
GHSA-834v-8h66-3h79highhappy-dom@20.14.5Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution
GHSA-xjj6-f33q-929chighundici@6.29.0undici vulnerable to Denial of Service via unhandled error on excessive content-encodings
GHSA-mrhx-c7xc-m3w5highzod@4.3.6Zod can exhaust memory while validating oversized constrained collections
GHSA-vcc3-ghjq-m6frmoderatedecode-uri-component@0.2.2decode-uri-component: Denial of service via exponential decoding of malformed percent-encoded input
GHSA-rj75-hqrm-r3gfmoderatepostcss-selector-parser@6.1.4PostCSS: Quadratic complexity in flat selector parsing allows CPU exhaustion
GHSA-4qv8-2p9j-2685moderateundici@7.30.0undici vulnerable to Denial of Service via unbounded memory in the EventSource parser
GHSA-f72v-6w45-q472moderateundici@7.30.0undici vulnerable to Subresource Integrity bypass via non-lowercase algorithm tokens
GHSA-2p8c-gr87-2676moderateelectron@41.10.7Electron: SameSite cookie restrictions are not applied to windows opened from links
GHSA-997q-gvvc-jwmxmoderateelectron@41.10.7Electron: openExternal permission requests can be attributed to the wrong origin
GHSA-fw47-9c62-h3vwmoderateelectron@41.10.7Electron: HID and USB permission checks can be attributed to the wrong origin
GHSA-x562-297c-g43gmoderateelectron@41.10.7Electron: Pointer lock permission requests can be attributed to the wrong frame
GHSA-w74x-v6f9-j3xgmoderatefast-uri@3.1.8fast-uri vulnerable to URI scheme spoofing via percent-encoded scheme decoding
GHSA-wjr9-j795-jxcrmoderatefast-uri@3.1.8fast-uri vulnerable to host confusion via unchecked WHATWG host canonicalization
GHSA-4qv8-2p9j-2685moderateundici@6.29.0undici vulnerable to Denial of Service via unbounded memory in the EventSource parser
GHSA-3qxv-5xgr-q7w9lowelectron@41.10.7Electron: Frame subscription trusts GPU-supplied frame geometry
GHSA-98mx-cjp9-gchmlowelectron@41.10.7Electron: Extension tabs.query returns tab URLs and titles without the tabs permission
GHSA-h3v5-h239-pg29lowelectron@41.10.7Electron: Off-screen rendering on macOS trusts GPU-supplied frame geometry
GHSA-333x-w4r6-6crplowfast-uri@3.1.8fast-uri vulnerable to scheme injection via an unescaped first-segment colon in suffix references
GHSA-xhq7-xmqj-769hlowfast-uri@3.1.8fast-uri vulnerable to improper path equivalence in relative reference normalization

By the numbers

Stars10K
Forks1,287
Contributors110
Commits2,497
Open issues261
Open pull requests269
Releases95
Latest releasev1.10.4
LicenceMIT
Main languageJavaScript
Project age1 year
Last pushOct 26, 2026
Tracked files3,185
Lines of code679.1K
Checkout size37 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.