Field report · 13 Aug 2026

Heard, Not Obeyed

I spent an evening trying to talk to my computer without touching it. The machine understood me the entire time. That turned out to be the hard part — because “it didn't hear me” and “it heard me and did nothing” look identical from the chair, and have nothing in common as bugs.

8 times the phrase
was recognized
0 messages
actually sent

macOS keeps that first number and shows it to nobody. Every hour I lost was spent not knowing it.

00 — Exhibit

What it looked like from the chair

Recorded during the evening described below — the setup in section 01, mid-investigation.

Screen recording of hands-free dictation on macOS. Opens a video player.
Screen recording · hands-free dictation, mid-investigation
01 — The answer

One command, every app

punch it

Apple Voice Control left listening continuously, and a single custom command bound to Cmd+Return. That is the entire working configuration. It sends in Claude, in Cursor, in Slack — one phrase, no per-app code, no background processes.

What it replaced: four background agents, eleven helper apps, and a local speech engine that had never successfully transcribed a single word.

The part that cost the evening: plain Return is swallowed by those apps. Cmd+Return is not. Same mechanism, same synthetic event, opposite outcome. I concluded from the first that keystrokes were closed off entirely, and that one over-generalisation sent everything down a different road for hours.

02 — The useful part

Reliability is a property of the binding

This is the piece worth stealing. A spoken command's reliability has almost nothing to do with the app it lands in and almost everything to do with the mechanism it is bound to. Sort your commands into these tiers and you can predict which ones work before testing any of them.

VerdictMechanismEvidence
Works Modified keystroke
Cmd+Return, Cmd+A/C/V/Z
Verified — 12 fires, messages landed in three apps
Works Accessibility action
click by label, native scroll
Verified — but needs the control to actually have a name
Works App launch Nothing in the path to swallow it
Fails Bare unmodified key
Return, Escape, PageUp/Down
Verified failing — 8 fires, 0 sends

So when a spoken command does nothing, check its binding tier before you debug the app. That is a five-second triage instead of an evening.

There is a nasty corollary. A custom command silently disables the built-in one that shares its phrase. I had scroll up and scroll down hand-bound to bare PageUp and PageDown — the failing tier — which left the operating system's own accessibility-based scrolling switched off. A working native action had been replaced by a broken keystroke, invisibly, and nothing anywhere said so. Check whether the platform already ships a verb before you bind it yourself.

03 — The catalog

Seven things that do not work

01 Impossible

A local speech-to-text app as the always-on ear

The whole category — MacParakeet, superwhisper, Wispr Flow — is push-to-talk or toggle. There is no open-mic mode to switch on. Excellent transcription, genuinely private, structurally unable to meet “don't make me touch a key.”

Check for a continuous mode before evaluating accuracy. Accuracy is worthless if the trigger is a key you already said you wouldn't press.

02 Silently ignored

Sending a synthetic plain Return

Claude, Cursor and Slack are web pages in desktop wrappers, with composers made of HTML rather than real controls. A plain Return posted programmatically is delivered and then quietly dropped. No error, no bounce, nothing in any log. This is the 8 / 0 above.

But Cmd+Return from the identical mechanism works in all three. The boundary is not native-versus-web and not real-versus-synthetic — it is which key. Skipping that one test made the next two entries look necessary.

03 Brittle

Walking the UI tree to find the Send button

The obvious workaround to #2: enumerate the interface, locate the send control, press it. It works often enough to ship and then fails constantly, because those trees shift with layout. My log was mostly no Send button and focus FAILED.

The supported version ships with the OS — say click plus whatever the button is called. It is genuinely good and it is not universal: it sent in Claude, and in Cursor it fired thirteen times and pressed nothing, because that send arrow is an icon with no accessible name. It removes per-app code, not the need for a per-app name.

04 Actively hostile

Auto-sending after a few seconds of silence

A daemon that submits when you stop talking will submit every time you pause to think. There is no threshold that separates finished from choosing a word. Mine was three seconds and it truncated sentences all evening.

An explicit trigger word is not friction to be engineered away. It is the only thing in the system that carries intent.

05 Redundant

Per-app daemons and helper scripts

Four background agents and eleven small apps, every one shaped around a single editor, all solving problems the dictation layer does not have — it already types into whatever holds focus. Two of the four had never been loaded at all, and nothing had noticed.

A component nobody noticed was missing was never doing work. Confirm what is actually running before you extend it.

06 Unreliable by design

Trigger phrases that sound like conversation

send it. that's it. go ahead. Two independent failures, both fatal. While transcribing, the recognizer has to decide whether your words are a command or the next part of your sentence — a coin flip, and it landed wrong about half the time. Worse, the phrases you say constantly fire mid-thought and submit half-written messages.

A trigger must be something you would never say in a sentence. Distinctive beats memorable. This is not a style preference; it is the whole difference between working and not.

07 The expensive one

Concluding one observation wider than the evidence

Three claims went into my notes and an early draft of this page, each corrected within the hour. The config is right, so it works — twice; the stored settings and the running engine are different layers. It works everywhere — written from someone saying it “seems to work,” a hedge upgraded into a verified claim. Keystrokes don't work here — true of Return, false of Cmd+Return.

Each was cheap to check and expensive to retract. The third is the costly kind: a bad generalisation early sends the whole investigation down a road, and the dead end you eventually hit reads like confirmation rather than the consequence of a wrong turn three steps back.

04 — Instrumentation

How to tell which half is broken

Every wasted hour came from not knowing whether the machine had heard me. It had. macOS keeps a counter for exactly this — one integer per command, incremented on every recognition — and surfaces it in no interface anywhere.

Read it, and the two failures separate instantly. The number moves and nothing happens: the command fired and the action is wrong. The number sits still: it never heard you, and the phrase is the problem. Those have nothing in common as bugs, and from the chair they are indistinguishable.

Whatever you are debugging, the equivalent counter almost certainly exists. Find it before forming a theory. I formed three.

05 — What it comes down to

Four rules that would have saved the evening

  1. Separate “heard me” from “did something.”They look identical from the chair and have nothing in common as bugs. Find the counter before forming a theory — this rule alone would have saved every hour lost here.
  2. A trigger word must be unsayable.If it can appear in a sentence, the recognizer cannot tell command from content, and you have built a coin flip.
  3. Never generalise from one key, one app, or one “seems to work.”Return failed and Cmd+Return worked. One app's button had a name and the next one's didn't. Each gap was a single test wide, and each wrong generalisation cost hours.
  4. The layer that reports success is the one lying.Settings correct, command fired, exit code zero — and no message sent. Only the outcome a person can see counts as evidence.
06 — What came out of it

I wrote all of it down

The reason this took an evening is that none of it is documented. There is no SDK — no framework, no XPC service, no entitlement. Voice Control commands live in a single undocumented property list, and the per-command schema, the part that decides whether your command works or fires forever doing nothing, is written down nowhere.

No community repository of shared command sets exists either. The search results are content farms, and a widely-copied claim that macOS has no custom vocabulary is simply false — the vocabulary pane has been there for years, and it syncs across devices.

So the notes became a project.

voice-mode

voice-mode →

The complete .voicecommands format, derived by reading a live command table and writing to it until every field's behaviour was known. The four mechanism tiers, so you can predict which of your commands will fail before testing any of them. The undocumented notify surface — five recognizer modes, including a code-dictation mode Apple never exposed in Settings. And a doctor that reads the recognition counter and tells you which half is broken.

Setup, the full command reference and troubleshooting live at justinfowler.com/voice-mode; the layered version of all of it is The Voice Optimization Stack. Source: GitHub.

MIT, public, and it found a bug in itself. Run against a machine with no configuration, the first version printed “no binding problems found” and exited clean — a pass on zero commands examined. That is the same shape as a test suite reporting zero violations out of zero items. It now refuses to call an empty set healthy.

Setup, as shipped. macOS 26.6, tested against Claude, Cursor and Slack. Apple Voice Control running continuously plus one command bound to Cmd+Return. No third-party dictation app, no background daemons, no per-application code. Surviving configuration: seventeen spoken commands and nothing else.