voice-mode
Talk to your Mac and have it actually do things. No hotkey to hold, no third-party app, no subscription. Five minutes to working, and a diagnostic that tells you why a command failed instead of leaving you guessing.
macOS already does this. Nobody tells you how.
Apple ships a feature called Voice Control that can drive your entire Mac by voice — type into any app, press any button, switch apps, edit text. It's free, it's built in, and it runs on-device.
It is also almost completely undocumented past the settings pane, and it has a handful of traps that make it look broken when it isn't. voice-mode is the missing manual plus the tools.
Two audiences, both covered on this page. If you just want to talk to your computer, do § 02 and stop. If you want to build your own command sets or understand the format, keep going to § 05.
A voice command bound to a plain Return is silently ignored by Claude, Cursor, Slack, VS Code, Discord and Notion. It doesn't error. The command is recognized. Nothing happens.
Bind it to Cmd+Return instead and it works in all of them. Same mechanism, same event, one modifier key. That single fact is most of what this project exists to tell you.
Working in five minutes
No terminal required for this part. You'll turn on Voice Control, import a set of commands, and say one phrase to send a message.
Turn on Voice Control
System Settings → Accessibility → Voice Control → switch it on. macOS downloads a small on-device model the first time. Nothing you say leaves your Mac.
Free up the microphone key, if you use one
Voice Control listens continuously — there is no key to hold. If you'd previously set up a push-to-talk dictation app, turn it off so the two don't fight over the microphone.
Download the starter command set
Grab sets/core.voicecommands from the repo. It's a plain file — ten commands, every one bound to a mechanism that works.
# or clone the whole thing
git clone https://github.com/justinfowler925/voice-mode
Import it
System Settings → Accessibility → Voice Control → Commands → Import, and pick that file. Your commands appear immediately, already switched on.
Say “punch it”
Click into any message box, talk normally, then say punch it. It sends. That works in Claude, Cursor, Slack — anywhere, with no per-app setup.
What to say
| Say this | It does | Notes |
|---|---|---|
| punch it | Sends the message | The one you'll use constantly. Works everywhere. |
| click <name> | Presses any labelled button | Built in. Say what's written on it: click Save. |
| show numbers | Numbers everything clickable | Built in. For buttons with no label — say the number. |
| show commands | Apple's full list | Built in, and app-specific. Your best reference. |
| scratch that | Deletes your last phrase | Built in. Faster than any correction. |
| go to cursor | Switches apps | One per app you use. |
| hands | Stop transcribing | Commands still work. Say this before talking about a command. |
| talk | Resume transcribing | |
| select all | Cmd+A | Then copy that, paste that, undo that. |
| go to sleep | Stop listening | Built in. wake up to resume. |
Why “punch it” and not “send it.” A trigger phrase competes with the sentence being transcribed — the recognizer has to decide whether your words are a command or more dictation, and a conversational phrase loses that call about half the time. Worse, phrases like that's it and go ahead occur in ordinary speech, so they fire mid-thought and submit half-written messages. Pick something you would never say in a sentence.
Troubleshooting, by symptom
Two completely different bugs look like this, and they have nothing in common. Either it never heard you, or it heard you and the action is wrong.
Run voice-mode doctor. It reads Apple's own recognition counter — a number macOS keeps and shows in no interface — and tells you which one you're in. If the count moved, the binding is wrong; if it never moved, the phrase is wrong.
You've almost certainly bound it to a plain unmodified key. Electron apps drop those silently.
Rebind to a modified key — Cmd+Return rather than Return. doctor flags every command in this category by name.
The phrase is too conversational, so the recognizer treated it as dictation.
Rename it to something unsayable. Two or more words, no single syllables — that's Apple's guidance too.
Same root cause, opposite direction: your trigger occurs in normal speech.
Rename it. And say hands before you discuss commands out loud, talk to resume.
That app's button has no accessible name, so there's nothing to match. Cursor's send arrow is exactly this.
Use show numbers and say the number, or bind the app's keyboard shortcut instead.
A custom command silently disables the built-in that shares its phrase — and the native one usually rides a better mechanism than whatever you bound.
Delete your custom version. doctor lists built-ins that have been switched off this way.
The format, and the tiers
There is no public API. No framework, no XPC service, no entitlement. Voice Control commands live in one undocumented property list, and the supported interface is the settings pane plus .voicecommands import/export.
The repo carries the complete per-command schema, derived by reading a live command table and writing to it until every field's behaviour was known. It isn't documented anywhere else.
Reliability is a property of the binding
Sort commands into these tiers and you can predict which of yours will fail before testing any of them.
| Verdict | Mechanism | Why |
|---|---|---|
| Works | Modified keystroke — Cmd+Return | Survives Electron composers. Verified. |
| Works | Accessibility action — click by label, menu item | Needs the control to have a name. |
| Works | App / URL launch | Nothing in the path to swallow it. |
| Fails | Bare unmodified key — Return, Escape | Silently dropped. Verified: 8 fires, 0 sends. |
Two stores, two shapes — the trap that eats your config
The export file wraps commands in a CommandsTable key. The live store is flat — command ids sit at the top level. Assume the wrapper when writing the live store and you create an empty table, orphaning every command you have, with no error anywhere.
# read the live store — there is no CommandsTable key here
defaults export com.apple.speech.recognition.AppleSpeechRecognition.CustomCommands -
# import MERGES: edits stick, deletions do not. Remove one command by key:
defaults delete <domain> Custom.808351004.970543146
# reload the recognizer. Do NOT also kill cfprefsd — it can undo your write.
killall DictationIM
The CLI
# diagnose every command by tier and fire count
./bin/voice-mode doctor
# readable JSON set -> importable .voicecommands
./bin/voice-mode build sets/core.json -o core.voicecommands
# run the gates (31 checks)
./tests/run
Python 3 standard library only, no dependencies. build refuses a bare-key binding rather than warning about it — you have to pass allow_bare to force one through.
Also in the repo: the undocumented notify control surface, with five recognizer modes rather than the two Settings exposes — including a code-dictation mode with its own language model that Apple never surfaced. Plus the full 161-command built-in catalog and the 82 nameable keys.
What this doesn't solve
Accuracy is the OS's, not mine. Expect roughly 95% on clear speech with a decent microphone in a quiet room, and noticeably worse with background noise. An external mic is the single biggest improvement available.
Proper nouns get mangled until you add them to Voice Control's vocabulary. That pane exists and syncs across devices — but its import file format is undiscovered, so words go in one at a time, or by voice with Text.AddSelectionToVocabulary.
Everything here is unsupported. No API means Apple can change the plist shape in any release. The format doc records how each field was verified so a future reader can tell what's fact and what's inference.
Local speech-to-text apps can't replace this. MacParakeet, superwhisper, Wispr Flow — the whole category is push-to-talk or toggle. Better transcription, but none of them do continuous hands-free, so none of them meet the bar.