Control API
Other programs on the Mac can start and stop Quack dictation, and can receive the text instead of having it typed. For example, a game-controller app can start dictation when a button is pressed and finish it when the button is released. They can also list, add, change, and remove dictionary words. For example, a script can read a codebase and add its project names, so that Quack spells them correctly. To test Quack without a speaker and a microphone, a program can dictate an audio file, or replay one of the user's recent dictations.
There are three ways to connect:
| Interface | Use it for |
|---|---|
| The control socket | Apps that hold a session, receive text, claim dictation, or edit the dictionary. |
quackctl | Scripts, agents, and languages that cannot open a Unix socket. |
quack:// links | One-shot triggers from Shortcuts, Stream Deck, or open. |
Permission
The first time a program sends a request or opens a link, Quack asks: Allow “App” to use Quack dictation? Quack remembers the answer. To change it, open Settings › Connected apps. Turning an app off or removing it disconnects it. A removed app is asked about again the next time it sends a request.
Quack attributes a request to the nearest app that launched the process, as macOS privacy prompts do. A Python script run in Terminal counts as Terminal. quackctl run by your controller app counts as the controller app. A process that no app launched is identified by its executable path. The open command often exits before Quack can trace it, so links that Quack cannot trace share one permission: Links from unidentified apps.
The permission prompt asks for consent. It is not a security boundary between programs that run as the same user.
Control socket
Connect to the Unix domain stream socket at:
~/Library/Application Support/Quack/control.sock
Only the user who runs Quack can connect. If the socket is missing, Quack is not running. Start it with open -g -b com.ericbellamy.Quack, then retry.
Each message is one JSON object on one line, ending with a newline (\n), in both directions. Requests are limited to 64 KB. A words reply can be longer. Quack ignores unknown fields. Clients must ignore unknown fields and unknown event types, because later versions can add them.
Requests
Every request has an op. The optional id string is copied into the reply to that request.
op | Fields | Effect |
|---|---|---|
start | deliver, partials, mute, audio, recording | Starts dictation that this connection owns. |
stop | Finishes dictation, like releasing the shortcut. | |
cancel | Discards dictation, like pressing Esc. | |
capture | enabled, partials, mute | Claims every dictation while this connection stays open. |
connect_t3 | none | Connects Quack to T3 on this Mac using T3’s supported local pairing CLI. Grants catalog read and task-running access with a permission ceiling matching saved local T3 defaults. Local launches use the project permission override, then the global T3 default. Verifies the environment and catalog before replying ok. Repeated calls reuse a verified connection. Does not send a task. |
status | Replies with Quack's state. | |
t3_threads | Replies with the T3 Code threads whose bubble is in color: finished, failed, or waiting for the user. | |
connect_t3_threads | Connects Quack to T3 Code for thread bubbles, as Settings › Threads › Connect T3 Code does. Quack asks T3's command-line tool for a token that reads threads and answers their approvals (orchestration:read, orchestration:operate) and follows the user's threads. Replies ok with t3 once connected. Separate from connect_t3, which lets Quack send tasks. | |
disconnect_t3_threads | Revokes that token in T3 and removes the bubbles. Replies ok with t3. | |
open_t3_thread | thread | Brings T3 Code forward at a thread Quack follows, as clicking the bubble does. It changes the user's screen, so ask first. |
words | Replies with the dictionary. | |
find | terms | Replies with the words that own each text. |
add | words | Adds dictionary words. |
edit | term, rename, heard_as | Changes a dictionary word. |
remove | terms | Removes dictionary words. |
recordings | limit | Replies with the recent dictations that Quack kept, or with limit, the newest that many. |
history | limit | Replies with the dictionary's changes, newest first. |
undo | change | Reverses a dictionary change. |
usage | Replies with how often each dictionary word fired. | |
probe | terms | Has macOS voices say each word, and replies with what Quack would type. Changes nothing. |
settings | Replies with every setting and its value, by name. See Settings. | |
update_settings | changes | Changes settings at once, as the Settings window does, and replies ok with every setting. |
deliver sets where the text goes:
type(default): typed into the focused app, as with the shortcut.return: sent only to your app. Nothing is typed, and Accessibility access is not needed.both: typed into the focused app and sent to your app.
partials (default true) sends stable words while the user is still talking. It applies only when your app receives the text.
mute (default true) mutes playing speakers and headphones while Quack listens, as the shortcut does, and restores them afterward. Quack mutes only output devices that are playing, and never virtual or aggregate devices. It mutes nothing when the user turns off Settings › Mute audio while dictating. Set mute to false to keep audio playing, for example game sound. Quack can then hear audio from speakers through the microphone. For capture, mute applies to every dictation the claim receives.
Automated tests and agents must not mute the user's speakers. Set mute to false, or set QUACK_DISABLE_OUTPUT_SILENCING=1 in the environment of quackctl, which then sends false. To stop a test build of Quack from muting anything, launch it with that variable or with open -g Quack.app --args -QuackDisableOutputSilencing YES. Quack reads the microphone directly and never opens an output device, so dictation with muting off does not touch playback.
Events
event | Fields | Meaning |
|---|---|---|
started | id, session, source, deliver | Dictation started. source is client or keyboard. |
partial | session, text | All stable words so far. Each event replaces the previous one. |
final | session, text, action, uncleaned | The full transcript, with any voice command removed, and with filler words and spoken corrections tidied when Clean up text is on. uncleaned is the transcript before that tidying, present only when it changed something. Partials are never tidied, so use uncleaned when you need the words exactly as spoken. |
ended | session, reason | Dictation is over. Always the last event of a session. |
ok | id, added, existing, ignored, history, t3 | The stop, cancel, capture, add, edit, remove, undo, connect_t3_threads, disconnect_t3_threads, or open_t3_thread request was accepted. add sets added, existing, and ignored. undo sets history to the new change that undid the old one. connect_t3_threads and disconnect_t3_threads set t3. |
status | id, status | Reply to status. |
words | id, words, missing | Reply to words or find. |
recordings | id, recordings | Reply to recordings. |
history | id, history | Reply to history. |
usage | id, usage | Reply to usage. |
probe | id, probes | Reply to probe. |
t3_threads | id, t3 | Reply to t3_threads. |
error | id, code, message | The request failed. message is readable text for a person. |
action is type, or the voice command the user ended with: send ("Send it") or scratch ("Trash what I said"). Quack does not press Return or delete anything for your app; decide what each command means.
reason is finished (a final came first), empty (too short to transcribe), cancelled, or failed.
status contains protocol (currently 3; version 2 cannot replay audio or keep dictionary history, and version 1 has no dictionary requests), model (ready, loading, downloading, or failed), microphone and accessibility (whether Quack has each permission), listening, and capturing (whether this connection holds the capture claim).
t3 contains state and threads. state is not_installed, disconnected, connecting, connected_waiting_for_t3 (connected; T3 Code isn't running, and Quack follows the threads again when it starts), connected, token_rejected (T3 revoked the token; connect again), or failed: and the reason. threads lists the threads whose bubble is in color, oldest first: id, title, project, and reason, which is finished, failed, input (the agent asked a question), approval (it asks to run a command, change a file, or use a tool), or plan (a plan is ready). computers, present when T3 Code is connected to other computers, lists each one Quack follows with environmentId, label, and state (connected, connecting, or failed: and the reason); computersProblem is computers_unavailable when Quack may not read T3's list of them (the keychain prompt was declined), until T3 is connected again. See T3 Code threads.
→ {"op":"t3_threads"}
← {"event":"t3_threads","t3":{"state":"connected","threads":[{"id":"8f0c…","project":"Quack","reason":"finished","title":"Fix the parser"}]}}
Error codes:
code | Cause |
|---|---|
bad_request | The line is not a valid request. |
not_allowed | The user has not allowed this app. |
busy | A dictation is already in progress. |
not_owner | stop or cancel without a dictation that this connection receives. |
capture_held | Another app holds the capture claim. |
not_ready | The speech model is still loading or downloading, or it failed to load. |
microphone_denied | Quack lacks Microphone access. |
accessibility_denied | type or both without Accessibility access. Use return, or grant access. |
microphone_failed | The microphone did not start. |
audio_failed | audio names a file that Quack cannot read as audio, or probe found no voice to speak with. |
not_found | edit names a word that is not in the dictionary, recording names no kept recording, or open_t3_thread names a thread Quack doesn't know. |
exists | edit would give a word text that another word owns, or undo would restore text that another word now owns. |
conflict | undo names a change whose words a later change touched. The message names the later change. |
Session rules
- One dictation runs at a time. While your app's dictation runs, the shortcut does not start or stop it. Esc still cancels it.
- If your connection closes during your dictation, Quack cancels it. A crashed app cannot leave the microphone on.
- In
typeandbothmodes, words appear in a pill while you speak. When dictation finishes, Quack pastes the complete transcript into whichever app has focus. Typing, clicking, and switching apps do not cancel dictation. Esc cancels it in every mode. Inreturnmode, Quack sends text to your app without pasting it. - Quack shows its listening badge during every dictation. It mutes playing speakers unless the request set
mutetofalseor the user turned muting off.
Capture
{"op":"capture","enabled":true} sends every dictation to your app, including dictation started with the shortcut or a link, and nothing is typed. The user's dictation arrives as started with source: "keyboard", then partial, final, and ended. You can stop or cancel it. The menu bar shows which app receives dictation.
One app at a time can hold the claim. It ends when you send {"op":"capture","enabled":false} or close the connection. Closing the connection during a captured dictation cancels that dictation.
Dictionary
A dictionary word is an object with term, the spelling Quack types, and heard_as, a list of ways the word gets misheard. Quack replaces each heard_as form with term. heard_as is optional in requests and can be empty. The forms are stored in lowercase, because matching ignores case. Settings shows them as Heard as.
No duplicates
Quack compares text the way it hears it: case, spaces, and punctuation are ignored. GitHub, github, and Git Hub are the same text, and so are cube control and Cube-Control. Each text belongs to at most one word, as its term or as one of its heard_as forms. Quack enforces this rule itself, so a client never has to check first:
- Adding a word that is already in the dictionary keeps the existing spelling. The reply lists it in
existing. Newheard_asforms are added to it. - A
heard_asform that sounds like the word itself, or like one of its other forms, is dropped. It adds nothing. - If another word already owns a term or form in an
add, the owner keeps it. Quack skips that text, adds the rest of the request, and lists the text inignoredwith the owning word. To move it, remove it from the owner withedit, then add it again. editrejects arenameorheard_asthat another word owns, withexists, and changes nothing.
Repeating an add therefore changes nothing, so a script or agent can run again safely. Settings follows the same rule. When Quack starts, it removes duplicates that a dictionary saved by an earlier version can hold. Text stays with the first word that claims it, so corrections do not change. A word spelled like an earlier word merges into it.
→ {"op":"add","words":[{"term":"kubectl","heard_as":["cube control"]},{"term":"Tauri"},{"term":"Cube Control"}]}
← {"added":["kubectl","Tauri"],"event":"ok","existing":[],"ignored":[{"text":"Cube Control","word":"kubectl"}]}
→ {"op":"find","terms":["Cube-Control","tauri","Rust"]}
← {"event":"words","missing":["Rust"],"words":[{"heard_as":["cube control"],"term":"kubectl"},{"heard_as":[],"term":"Tauri"}]}
→ {"op":"words"}
← {"event":"words","words":[{"heard_as":["cube control"],"term":"kubectl"},{"heard_as":[],"term":"Tauri"},…]}
find takes a list of texts in terms. It replies with each word that owns one of them as its term or a heard_as form, once per word, and lists the texts no word owns in missing.
Changes
addputs new words at the top of the dictionary, in request order.editchanges the word named byterm.renamerespells it, for exampleGithubtoGitHub.heard_asreplaces all of its forms;[]removes them. Omitted fields stay the same.removeremoves the words whose terms match, and ignores the rest.- A term has at most 64 characters on one line, with at least one letter or digit. Quack trims surrounding whitespace and collapses inner whitespace. If one word is invalid, Quack rejects the whole request with
bad_requestand changes nothing. - Changes are saved, appear in Settings, and apply to the next dictation. A dictation in progress keeps the dictionary it started with.
- Dates in replies are ISO 8601 text in UTC, such as
2026-10-18T21:42:39Z.
History
Quack keeps every dictionary change permanently, whoever made it: a person in Settings, the Add to Quack Dictionary service, an app through this API, or Quack itself when it removes duplicates. Each request that changes the dictionary is one change. quackctl add sends a long list as several requests, so it can make several changes.
{"op":"history"} replies with the changes, newest first. limit returns only the newest that many. Each change has these fields:
| Field | Meaning |
|---|---|
id | Counts up from 1. Names the change in undo. |
date | When it happened. |
by, by_id | Settings, Services menu, Learned from your fix, Quack, or the app's name, and the app's bundle ID or executable path when an app made it. |
summary | A readable description, such as Added Tauri (heard as tory). |
undoes | The change this one undid, if it is an undo. |
changes | Each word before and after: word_id, before and after (as in words replies; missing when the change added or removed the word), and index, the word's position before the change. |
{"op":"undo","change":12} reverses change 12 as a new change, which can itself be undone. A removed word returns where it was. Quack refuses with conflict if a later change touched the same words, and names that change, so an undo never overwrites later work. Undo the later change first, or edit the word. Settings shows the history under Dictionary › History…, with an Undo button on each change.
→ {"op":"history","limit":1}
← {"event":"history","history":[{"by":"Terminal","by_id":"com.apple.Terminal","changes":[{"after":{"heard_as":["tory"],"term":"Tauri"},"index":0,"word_id":"7F0B…"}],"date":"2026-10-18T23:28:05Z","id":53,"summary":"Added Tauri (heard as tory)"}]}
→ {"op":"undo","change":53}
← {"event":"ok","history":[{"by":"Terminal",…,"id":54,"summary":"Undid change 53: Removed Tauri","undoes":53}]}
Usage
Quack counts how each dictionary word fires in the user's dictations, so that an agent can find words that never help and words that fire where they shouldn't. Quack counts only microphone dictations, not replays or audio files. {"op":"usage"} replies with one entry per word, in dictionary order:
| Field | Meaning |
|---|---|
word_id, term | The word. |
uses | Dictations in which the word fired in any way. |
spelled | Times the transcript spelled the word itself, perhaps with other case, spacing, or punctuation. |
forms | Times each heard-as form fired, such as {"tory":3}. |
near_misses | Times a word of six or more letters matched text a letter or two off, by the text heard, such as {"prism":2}. Ordinary words here are false matches. |
steered | Times dictionary biasing steered the speech model to the word. |
last_used, since | When the word last fired, and when Quack began counting it. |
recordings | Up to five kept recordings in which it fired, newest first, to replay. |
learned | Its heard-as forms that Quack learned from the user's fixes, when it has any. |
The reply's fixes lists fixes the user made once, which Quack learns if they make them again, such as {"count":1,"date":"…","heard":"jean","term":"Jeanne"}, where count is how many times the user has made it. Quack learns each one if the user makes the same fix again. See Learning from your fixes.
A word that never fires costs almost nothing. A heard-as form or near miss that matches ordinary speech changes text the user meant, every time. To check one, replay its recordings, then edit or remove the word. History can undo the change if it was wrong.
Probe
Before adding a word, an agent can hear how Quack hears it. {"op":"probe","terms":["Tauri","Siobhan"]} has macOS speech synthesis say each word in three sentences, in the voices Samantha, Daniel, Karen, and Moira (American, British, Australian, and Irish English). Quack transcribes each sentence as dictation would with the word in the dictionary, and replies with what it typed in the word's place. It takes up to 20 words, about a second each, and changes nothing. Dictation keeps working meanwhile.
→ {"op":"probe","terms":["Tauri","Siobhan"]}
← {"event":"probe","probes":[{"clips":12,"heard":[{"count":6,"ordinary":true,"text":"tory"},{"count":2,"ordinary":true,"text":"torah"}],"right":4,"term":"Tauri"},{"clips":12,"heard":[{"count":3,"ordinary":false,"text":"chauvon"},{"count":2,"ordinary":false,"text":"chavon"}],"right":7,"term":"Siobhan"}]}
| Field | Meaning |
|---|---|
term | The word. |
clips | Sentences said, each in each installed voice. |
right | Clips in which Quack typed the word as spelled. |
heard | What Quack typed instead, most often first: text, lowercased as heard-as forms are stored, and empty when nothing was typed; count; and ordinary, true when every word of it is an ordinary English word. |
Use it to choose heard-as forms. A form that is not a word, such as "chavon", rarely matches anything else the user says, so it is safe to add. A form marked ordinary, such as "tory", would turn every "Tory" the user says into Tauri. Leave it out, and let the user's fixes teach it, or ask the user. A word that is right in every clip needs no form.
A probe predicts real speech only roughly. The voices read the spelling, so a word spelled unlike it sounds, such as Vercel, is said wrong. They have no accent, and real speakers mishear in their own ways: on the accent benchmark (scripts/accent-eval), another speaker's most common misheard form matched about 7% of a speaker's misses with the dictionary on. A word the voices get right can still be misheard; the user's fixes catch those (see Learning from your fixes).
Large dictionaries
Quack handles dictionaries of 100,000 words. Correcting a transcript takes the same time at any size. Each change rebuilds the dictionary's indexes in the background, which takes about 25 ms at 25,000 words and 200 ms at 100,000. The change applies once the indexes are ready, so a dictation that starts within that moment can still use the previous dictionary. Settings lists the newest 200 words and searches the rest.
Size costs accuracy rather than speed. Every word is a chance for the speech model to be steered toward it, and every heard-as form rewrites the words it matches. Use usage to remove words that misfire.
Settings
{"op":"settings"} replies with a settings event: settings holds every setting the Settings window and the menu bar panel offer, by the name it has in Quack's settings.json, and choices.microphones lists this Mac's microphones (uid, name) for microphoneUID. A setting left unset, such as an automatic launcher, is null.
{"op":"update_settings","changes":{"sleepingDuckBrightness":0.3,"showsSleepingDuck":true}} changes the named settings. They apply at once and are saved, as when the user changes them in Settings; nothing restarts. Quack refuses the whole request with bad_request if a name is unknown or a value is of the wrong type, out of range, or not among a setting's choices, and the message says what the setting takes. Otherwise it replies ok with every setting afterwards.
quackctl settings describe lists each setting with what it does and the values it takes; the list lives in Sources/QuackCore/SettingsAPI.swift. The dictionary isn't a setting here; use its own requests. Which apps may control dictation can't be changed through the API, so no program can grant itself or another one access.
→ {"op":"update_settings","changes":{"sleepingDuckBrightness":0.3}}
← {"event":"ok","settings":{"sleepingDuckBrightness":0.3,"showsSleepingDuck":true,...}}
→ {"op":"update_settings","changes":{"sleepingDuckSpeed":10}}
← {"code":"bad_request","event":"error","message":"sleepingDuckSpeed can't be 10. Duck speed, as a multiple of normal speed. From 0.25 to 3."}
MCP server
quackctl mcp is an MCP server on standard input and output, so agents such as Claude Code and Codex can use Quack as tools: get_settings and update_settings, get_status, and the dictionary's list_words, find_words, add_words, edit_word, remove_words, probe_words, dictionary_history, undo_dictionary_change and word_usage. Each tool sends the request of the same purpose above over the control socket, starting Quack if it isn't running, so the first call from an agent asks for permission like any other app. Add it to Claude Code with:
claude mcp add --scope user quack -- /Applications/Quack.app/Contents/MacOS/quackctl mcp
and to Codex in ~/.codex/config.toml:
[mcp_servers.quack]
command = "/Applications/Quack.app/Contents/MacOS/quackctl"
args = ["mcp"]
The tools are defined in Sources/QuackCore/MCPTools.swift and served by Sources/quackctl/MCP.swift.
Testing with audio
start can play audio instead of listening to the microphone. Set one of these fields:
audio: the absolute path of an audio file that macOS can read, such as WAV, AIFF, M4A, or FLAC.recording: theidof a kept recording.
Quack plays the audio at the speed it was spoken, so partials and the pill behave as they do for speech. Dictation finishes at the end of the audio, and the events are the same as for the microphone. You can stop or cancel it sooner. Playing audio needs no Microphone access and does not mute audio output. Quack does not record pace for it or keep it as a recording. Use deliver: "return" so that nothing is pasted into the focused app.
→ {"op":"start","deliver":"return","audio":"/tmp/test.aiff"}
← {"deliver":"return","event":"started","session":7,"source":"client"}
← {"event":"partial","session":7,"text":"Deploy with"}
← {"action":"type","event":"final","session":7,"text":"Deploy with Tory."}
← {"event":"ended","reason":"finished","session":7}
Kept recordings
Quack keeps the user's last 1,000 dictations, and each new one replaces the oldest. The audio is compressed losslessly, so a 10-second dictation takes about 190 KB and a thousand take about 200 MB. Recordings kept before 1.14.1 are not compressed. Each recording is encrypted with a key in the user's keychain, so only Quack can play it. Quack keeps a dictation only after it is transcribed into some text.
{"op":"recordings"} replies with the kept recordings, newest first; {"op":"recordings","limit":50} with the newest 50. Each one holds everything Quack knew about the dictation:
| Field | Meaning |
|---|---|
id | Names the recording in start. |
date, seconds | When the dictation finished, and the length of its audio. |
heard | The model's transcript, before Quack removed a voice command and replaced heard-as forms. The dictionary has already biased the model toward its words. |
words | The words of heard, each with its start and end in seconds. |
text, action | What Quack delivered, and type, send, or scratch, as in the final event. |
routing_vocabulary | Catalog computer, model, and project names used as recognition hints. Replays retain this snapshot. Absent on older recordings and dictations without routing hints. |
screen_words | Names and terms read from the window Quack typed into, which biased the dictation besides the dictionary. A replay of the recording uses the same ones. |
uncleaned | The text before Clean up text tidied it into text. Present only when cleanup changed the text. |
source, client, app | What started the dictation (keyboard, link, or client), the app that controlled it or received its text, and the bundle ID of the app Quack typed into. Each of the last two is present only when it applies. |
page | The top-level web page in the focused window when a typing dictation started, when Accessibility exposes it. Contains domain and an optional title, for example {"domain":"mail.google.com","title":"Inbox - Gmail"}. URL paths, query strings, fragments, and credentials are discarded. Captured even when screen words are off. Absent for return-only dictations and older recordings. |
microphone, peak_db | The microphone, and the loudest sample in dB below full scale. Near 0 means clipping. Below about -30 means the microphone heard the speaker faintly. |
model, quack_version | The speech model revision and the Quack version. |
corrections | Each dictionary word that changed the text, in order: its term, its kind (term, form, near_miss, screen for a term shown on screen, name for a project, model or app name capitalized, or steered), the form that fired, and the heard words it matched. |
dictionary_change | The newest dictionary change when the dictation finished. history up to it gives the dictionary that was in use. |
→ {"op":"recordings"}
← {"event":"recordings","recordings":[{"action":"type","app":"com.apple.Terminal","corrections":[],"date":"2026-10-18T21:42:39Z","dictionary_change":41,"heard":"Deploy the app with Tori and Vaughn.","id":"20261018T144239","microphone":"MacBook Pro Microphone","model":"Phonon-2-1143812f…","peak_db":-9.5,"quack_version":"1.5.3 (18)","seconds":3.69,"source":"keyboard","text":"Deploy the app with Tori and Vaughn.","words":[{"end":0.52,"start":0.21,"text":"Deploy"},…]}]}
When the user says that Quack misheard a recent dictation, an agent can fix the dictionary and check the fix:
1. Find the dictation by its text or heard. 2. Replay it with start and recording to see what Quack hears now. 3. add the right words, with the misheard text from heard as heard_as. If heard is right and text is wrong, a heard-as form made the mistake: edit that word. 4. Replay it again. The dictionary applies to the next dictation, so the replay shows the fix.
If an earlier change made the mistake, for example a heard-as form that rewrites an ordinary word, find it with history and reverse it with undo.
Example
A controller button that dictates into a chat field inside your app:
→ {"op":"start","id":"a","deliver":"return"}
← {"deliver":"return","event":"started","id":"a","session":4,"source":"client"}
← {"event":"partial","session":4,"text":"Meet at the"}
→ {"op":"stop"}
← {"event":"ok"}
← {"action":"send","event":"final","session":4,"text":"Meet at the north gate."}
← {"event":"ended","reason":"finished","session":4}
Minimal Python client:
import json, os, socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(os.path.expanduser("~/Library/Application Support/Quack/control.sock"))
events = sock.makefile("r")
def send(**request):
sock.sendall((json.dumps(request) + "\n").encode())
send(op="start", deliver="return")
input("Talk, then press Return. ")
send(op="stop")
for line in events:
event = json.loads(line)
if event["event"] == "final":
print(event["text"], event["action"])
if event["event"] in ("ended", "error"):
break
quackctl
quackctl is inside the app at /Applications/Quack.app/Contents/MacOS/quackctl. Quack also links it into a folder on the shell's PATH: ~/.local/bin if that folder exists, otherwise /opt/homebrew/bin or /usr/local/bin. Quack updates the link when the app moves, and never replaces a quackctl that is not its own. Running the app itself from a shell, as in Quack.app/Contents/MacOS/Quack --help or Quack recordings, runs quackctl. If Quack is not running, quackctl starts it in the background.
quackctl --help begins with the steps for fixing a word that Quack mishears. The Recordings folder in ~/Library/Application Support/Quack holds a README.txt that points to the same commands.
quackctl status # Prints the status event.
quackctl dictate # Prints the final text. Return or end of input finishes.
quackctl dictate --deliver type # Types into the focused app instead.
quackctl dictate --json # Prints every event as a JSON line.
quackctl dictate --no-mute # Keeps audio playing while listening.
quackctl dictate --audio test.aiff # Dictates a file instead of the microphone, and prints the text.
quackctl dictate --recording 1 # Replays the newest kept recording. Also takes an ID.
quackctl connect-t3 # Connect Quack to T3 locally, without a browser or pasted code.
quackctl threads # Lists the T3 Code threads with a colored bubble: ID, reason, project and title.
quackctl threads connect # Connects Quack to T3 Code for thread bubbles. `disconnect` undoes it.
quackctl threads open ID # Brings T3 Code forward at that thread. Ask the user first.
quackctl recordings # Lists the newest 50 kept recordings: number, ID, date, length, and what Quack heard and typed.
quackctl recordings --all # Lists every kept recording.
quackctl recordings --cleaned # Only the dictations Clean up text changed, with the text before it, from every kept recording.
quackctl recordings --unknown # Only dictations with words that are neither English nor a term Quack knew of, which are often mishearings.
quackctl recordings --since 1.16.0 # Only dictations made with that version of Quack or a later one. Combines with the other filters.
quackctl recordings --json # Prints every detail of each recording.
quackctl capture # Prints every dictation as JSON lines until Ctrl-C.
quackctl connect # Relays JSON lines between standard input and the socket.
quackctl words # Prints the dictionary, one word per line.
quackctl find "cube control" Tauri # Prints the words that own each text. Exits 1 if any is missing.
quackctl add Tauri Bun # Adds words, and prints what it added and skipped.
quackctl add kubectl --heard-as "cube control" --heard-as "cube cuddle"
quackctl edit Github --rename GitHub # Respells a word.
quackctl edit kubectl --heard-as "cube cuddle" # Replaces its heard-as forms. --no-heard-as removes them.
quackctl remove Tauri Bun # Removes words.
quackctl history # Lists the newest 50 dictionary changes. --all lists every one.
quackctl undo 53 # Reverses change 53.
quackctl usage # Shows how often each word fired, and how. --unused lists words that never fired.
quackctl probe Tauri Siobhan # Prints what Quack types when macOS voices say each word. --json prints the reply.
dictate reads standard input: any input, or the end of input, finishes dictation. With --audio or --recording, it ignores standard input and finishes at the end of the audio. A relative --audio path is resolved against the current directory. Ctrl-C, SIGTERM, or SIGHUP cancels it. To drive it from another program, start quackctl dictate with a pipe as standard input when the button goes down. Write a newline when it comes up to finish, or terminate the process to cancel. Exit status: 0 finished, 1 error, 2 usage error, 3 cancelled.
words prints a word with heard-as forms as kubectl = cube control, cube cuddle. Without words as arguments, add and remove read one word per line from standard input, in the same format. quackctl words > words.txt saves the dictionary, and quackctl add < words.txt restores it. Long input is sent as several requests. add prints the words it added, the words already in the dictionary, and each text it skipped because another word owns it. --json prints Quack's replies instead. The other changes print nothing when they succeed. Every dictionary command exits with status 1 and a message when Quack refuses a request.
An agent can hear a word before adding it, and add only forms that are safe:
quackctl probe Siobhan
# Siobhan: right in 7 of 12
# chauvon ×3
# chavon ×2
quackctl add Siobhan --heard-as chauvon --heard-as chavon
probe marks forms made of ordinary words, such as "tory" for Tauri. Adding one would rewrite those words whenever the user says them.
An agent can also test a fix without playing sound, for example with a clip made by say:
say -o /tmp/test.aiff "Ask Siobhan about it"
quackctl dictate --audio /tmp/test.aiff # Ask Chavon about it.
quackctl add Siobhan --heard-as chavon
quackctl dictate --audio /tmp/test.aiff # Ask Siobhan about it.
An agent or script can fill the dictionary from a project, for example with names from package.json:
jq -r '.dependencies // {} | keys[]' package.json | quackctl add
Links
| Link | Effect |
|---|---|
quack://start | Starts dictation that types into the focused app. |
quack://stop | Finishes it. |
quack://toggle | Starts dictation, or finishes it if it is running. |
quack://cancel | Discards it. |
Run open -g quack://toggle so that Quack does not take focus. Links act like the shortcut: if an app holds the capture claim, that app receives the text. Links never stop dictation that a socket client started. Pressing the shortcut finishes a dictation that a link started. Links cannot return text; use the socket or quackctl for that.