Vibecode Superscribe
track this build4 phases, 12 steps, beginner friendly0%The desktop dictation half is a solved one-sitting build with complete open-source clones to fork. The hero product is not that: live-transcribing calls on your existing iPhone number takes carrier call forwarding, a pooled Twilio number, a CallKit/PushKit softphone, and a dual-track media-stream pipeline that can only be debugged against live phone calls. And a clone gets you a transcript file, not the workspace around it: roughly 45k lines of API and web app that embed every recording, time block, and project (with synced GitHub repo context), auto-file notes and billable time to the right client, and expose it all as reports, semantic search, a public API, and an MCP server. Each piece is a documented recipe; shipping all of them integrated is the multi-week part even before telephony.
You are building a lean indie version of Superscribe. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Superscribe · indie build A dictation tool whose hard problems are capturing only while a key is held without becoming a keyboard observer, keeping an honest record of audio that leaves the machine for a transcription service, and inserting text into another application without stealing or losing the clipboard. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A native macOS menu bar application built with Swift Package Manager | dictation has to be instant and available everywhere, which rules out a web page | | Hotkey | A registered system hotkey, not an event tap observing all input | an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop | | Capture | Audio engine opened on key down and closed on key up, with the state shown | the microphone should be closed whenever you are not holding the key | | Transcription | One streaming provider with a local model as an equal path, chosen per dictation | the choice between the two is a privacy decision the user should make, not a fallback | | Insertion | Paste on release with the previous clipboard restored after a confirmed paste | word-by-word insertion into someone else's text field is where these tools corrupt documents | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running a current macOS, with a second machine or clean user account for permission testing** · free - Why: The permission prompts and secure input behaviour are the interesting part of this build and they only test properly on an account that has not already granted everything. - Get it: Create a fresh user account you can reset. - Verify: A first run on the clean account prompts for microphone access and nothing else until insertion is used - [ ] **A Swift toolchain with Swift Package Manager** · free - Why: Builds the menu bar application without an Xcode project to maintain. - Get it: Install the command line developer tools. - Verify: swift build produces a runnable binary - [ ] **A streaming transcription API key** (optional) · pay per minute of audio; the monthly ceiling in this build is what keeps it bounded - Why: Remote mode is the fast path, and this is the only component that costs money or sends audio anywhere. - Get it: Create a key with a provider whose retention policy you have read, and record what it says. - Verify: A held key produces a transcript and a transmissions row - [ ] **A local speech model you can run on the machine** · free - Why: It is a selectable mode rather than a fallback, and a local path nobody tested is the same as not having one. - Get it: Download a small model and a runtime that can transcribe a wav file offline. - Verify: Local mode produces a transcript with the network disconnected - [ ] **What this tool is allowed to capture and keep, written before the first build** · free - Why: Audio, transcripts and retention windows are much easier to decide now than after a month of dictations exists on the disk. - Get it: Write down whether audio is kept at all, for how long, and what the redaction patterns are. - Verify: The settings match the written policy and the README states it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. - call capture on your existing number (carrier forwarding into a CallKit softphone) - glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables - auto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging - the workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server - Windows parity and signed, notarized auto-updating installers If one of those is essential to you, that is the reason to keep paying for Superscribe, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Superscribe The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a push-to-talk dictation tool that records only while a key is held and says exactly what left the machine to replace Superscribe. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool opens a microphone and puts text into whatever window has focus, which makes two things matter more than any feature. Push to talk, never always listening. Recording starts when the key goes down and stops when it comes up, and there is no mode that opens the microphone and waits for a wake word. macOS shows an orange dot beside the Control Centre icon while the microphone is in use, but Apple's own documentation says only one privacy indicator is shown at a time, so a camera in use replaces it with a green dot. The operating system's indicator is a courtesy, not your permission model: show your own recording state, and capture nothing outside a held key. Second, this is not a keylogger and must not grow into one. Apple's Terminal offers Secure Keyboard Entry specifically to prevent other apps detecting and recording what you type, and advises checking that nothing depends on receiving those keystrokes first. Your hotkey will stop working in that window, and that is correct behaviour rather than a bug to route around. Register a hotkey rather than observing every key, never log keystrokes, and never read the contents of the focused window. And name what leaves. Audio sent to a transcription service is a recording of whatever was in the room, including the other half of a conversation. Record what was sent, to whom, and how long it was, and keep the local transcription path working rather than treating it as a fallback nobody tests. ### Stack (fixed, do not substitute) - Runtime: A native macOS menu bar application built with Swift Package Manager · dictation has to be instant and available everywhere, which rules out a web page - Hotkey: A registered system hotkey, not an event tap observing all input · an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop - Capture: Audio engine opened on key down and closed on key up, with the state shown · the microphone should be closed whenever you are not holding the key - Transcription: One streaming provider with a local model as an equal path, chosen per dictation · the choice between the two is a privacy decision the user should make, not a fallback - Insertion: Paste on release with the previous clipboard restored after a confirmed paste · word-by-word insertion into someone else's text field is where these tools corrupt documents ### Data model (create this before Phase 1) - dictations(id, started_at, ended_at, duration_ms, mode, provider, model_id, transcript_path, cleanup_applied, target_bundle_id, cost_minor) - transmissions(id, dictation_id, provider, endpoint_host, bytes_sent, seconds_sent, retention_note, at) · one row per time audio left the machine, written before the socket opens - audio_files(dictation_id, path, bytes, delete_after) · optional, off by default, and deleted on a timer when on - redactions(pattern, label, added_on) · applied to the transcript before it is stored or shown in history - apps(bundle_id, first_seen_at, insertion_mode, blocked) · per-application behaviour is explicit rather than guessed - settings(hotkey, provider, retain_audio, transcript_retention_days, monthly_ceiling_minor) - There is no keystroke table, no window content and no audio outside the retention setting. A dictations row records that something happened; the transcript itself is optional. ### Phase 1 · A microphone that is shut unless you are holding the key Build: Register a system hotkey rather than installing an input tap. Open the audio engine on key down, close it on key up, and drive a menu bar state from the engine's actual running state rather than from your own flag. Handle the key being released while the window focus changed, the machine sleeping mid-capture, and the audio device disappearing, all by closing the engine. Trap: Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. Done when: The application installs no keyboard event tap, verified by review and by the absence of the corresponding permission prompt on a fresh machine. The menu bar state is derived from the audio engine's running state and a forced engine stop changes it within a stated interval, tested. Sleeping the machine mid-capture closes the engine and discards the partial recording, tested. In a window with secure keyboard entry enabled the hotkey does not fire and the application explains why rather than failing silently. Do not build yet: No transcription, no insertion, no history yet. ### Phase 2 · What left the machine, and when Build: Write the transmissions row before opening the socket, holding the provider, the endpoint host, the seconds and bytes to be sent, and a note of that provider's stated retention. Offer a local model as a selectable mode rather than a fallback, and test both paths in the suite. Show, per dictation in the history, whether audio left the machine and where it went. Trap: Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. Done when: A transmission row exists before any audio leaves, tested by killing the process between the write and the socket opening and finding the row. Local mode produces a transcript with no network activity at all, verified by watching the interface during a test. Each history entry displays the provider and endpoint host or the word local, checked in the interface test. An incomplete transmission row is displayed as uncertain rather than as not sent. Do not build yet: No cleanup pass, no insertion, no retention yet. ### Phase 3 · Getting the text into the other application Build: Run one cleanup pass over the final transcript, apply the redaction patterns, then save the current clipboard, place the text, paste, confirm the paste happened, and restore the previous clipboard only after confirmation. Maintain per-application behaviour in the apps table, and allow an application to be blocked entirely. Never insert word by word while the user is still speaking. Trap: Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. Done when: The previous clipboard is restored only after the paste is confirmed, tested with a deliberately slow target. When confirmation does not arrive within the cap, the transcript stays on the clipboard and the interface says so, tested. A blocked application receives no insertion and the transcript goes to history instead, tested. Nothing is inserted before the key is released, asserted in the suite. Do not build yet: No retention policy, no cost ceiling, no export yet. ### Phase 4 · History is a liability, so treat it like one Build: Apply redaction patterns before a transcript is written to disk, not when it is displayed. Keep audio only when explicitly enabled, and delete it on a timer whether or not the application is running at the time. Expire transcripts on a configured window. Provide a delete-everything action that removes files as well as rows and reports what it removed. Trap: Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. Done when: A dictation containing a redaction pattern leaves no unredacted text anywhere on disk, tested by searching the transcript directory afterwards. Audio is absent unless retention is explicitly enabled, and enabled audio is deleted at its deadline even if the application was closed when it passed, tested. Transcript expiry removes files and rows together, tested. Delete-everything removes files and rows and reports counts for both, tested. Do not build yet: No cost ceiling, no export, no packaging yet. ### Phase 5 · Permissions, cost and a way out Build: On first run, explain each permission the application asks for in one sentence each, and make the application work in local mode with the fewest of them. Track remote transcription spend per month and switch to the local model at the ceiling rather than stopping. Export the history as newline-delimited JSON with the transmission records included. Document what the app cannot see. Trap: Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. Done when: Each permission is requested at first use with a one-sentence explanation, and refusing any one leaves the application usable in a described reduced mode, tested by denying each in turn. Reaching the monthly ceiling switches to local transcription with a visible notice rather than stopping dictation, tested. Export includes dictations and transmissions and reimports into an empty store, tested. The README lists what the application cannot see, and that list is checked against the code by review. Do not build yet: Nothing after this. ### Out of scope (and why) - Any always-listening or wake-word mode, because the entire safety story here rests on the microphone being closed unless a key is held. - Capturing telephone calls on your existing number, which needs carrier forwarding, a softphone and a media pipeline that can only be debugged against live calls, and which brings recording-consent law in with it. - Live word-by-word insertion while speaking, which is where these tools corrupt documents in editors and terminals and is not worth the demo. - Reading the focused window's contents for context, which is a far larger permission than dictation needs and changes what this application is. ### README must contain - That the microphone is open only while the key is held, and that the application shows its own state rather than relying on the system indicator - That the application registers a hotkey and installs no keyboard tap, and that this is why it does not fire in secure input windows - Which provider audio is sent to in remote mode, what that provider says about retention, and where the local record of each transmission lives - That redaction happens before anything is written to disk, and what the transcript and audio retention windows are - What the application cannot see: keystrokes, window contents, and anything outside a held key ===== AGENTS.md ===== # Agent instructions · Superscribe indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A native macOS menu bar application built with Swift Package Manager, A registered system hotkey, not an event tap observing all input, Audio engine opened on key down and closed on key up, with the state shown, One streaming provider with a local model as an equal path, chosen per dictation, Paste on release with the previous clipboard restored after a confirmed paste. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ===== BUILD_PLAN.md ===== # Build plan · Superscribe A dictation tool whose hard problems are capturing only while a key is held without becoming a keyboard observer, keeping an honest record of audio that leaves the machine for a transcription service, and inserting text into another application without stealing or losing the clipboard. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A microphone that is shut unless you are holding the key Capture begins on key down and ends on key up, the application never observes other keystrokes, and the visible state always matches the hardware. ### Steps 1. Register a hotkey, never tap all input The permission you are not asking for is the point. Files: `Sources/Hotkey/Register.swift`, `Tests/NoEventTapTests.swift` 2. Derive the indicator from the engine, not a flag A flag and a microphone can disagree, and the flag is the one that lies. Files: `Sources/Capture/Engine.swift` 3. Close the engine on sleep, focus loss and device change Every one of these has left a microphone open in some shipped app. Files: `Sources/Capture/Lifecycle.swift` ### Done when - [ ] No keyboard event tap is installed - [ ] The indicator follows the engine's real state - [ ] Sleep mid-capture closes the engine and discards audio - [ ] Secure input windows are explained, not worked around ### Watch out - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. ## Phase 2 · What left the machine, and when Every transmission to a remote service is recorded before it happens, and the local path works as well as the remote one. ### Steps 1. Write the transmission row before the socket Afterwards is too late for the only case that matters. Files: `Sources/Transcribe/Remote.swift`, `Tests/TransmissionRecordTests.swift` 2. Make the local model a mode, not a fallback A path nobody tests is a path that does not work. Files: `Sources/Transcribe/Local.swift` 3. Show provenance on every history entry The honest answer to where did that go is in the list. Files: `Sources/UI/HistoryView.swift` ### Done when - [ ] The transmission row precedes the socket - [ ] Local mode produces no network activity - [ ] History shows provider and endpoint or local - [ ] An incomplete transmission reads as uncertain ### Watch out - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. ## Phase 3 · Getting the text into the other application The transcript arrives in the focused application without stealing the clipboard or corrupting a field. ### Steps 1. Confirm the paste before restoring the clipboard A timer-based restore eats the dictation on a busy machine. Files: `Sources/Insert/Paste.swift`, `Tests/ClipboardRestoreTests.swift` 2. Keep per-application behaviour explicit Terminals, editors and browser fields all behave differently. Files: `Sources/Insert/AppPolicy.swift` 3. Never insert before the key is released Live insertion into someone else's document is how text gets mangled. Files: `Sources/Insert/Gate.swift` ### Done when - [ ] Clipboard restore waits for a confirmed paste - [ ] An unconfirmed paste leaves the transcript on the clipboard - [ ] Blocked applications receive no insertion - [ ] Nothing is inserted before key release ### Watch out - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. ## Phase 4 · History is a liability, so treat it like one Transcripts and audio expire, redaction happens before storage, and deleting really deletes. ### Steps 1. Redact before writing, never at display The file is what other software reads. Files: `Sources/Store/Redact.swift`, `Tests/RedactionOnDiskTests.swift` 2. Delete expired audio on next launch as well as on a timer A deadline that passes while the app is closed still passed. Files: `Sources/Store/Retention.swift` 3. Make delete-everything report what it removed A silent delete is indistinguishable from a broken one. Files: `Sources/UI/PrivacyPane.swift` ### Done when - [ ] No unredacted transcript exists on disk - [ ] Audio is absent unless explicitly retained - [ ] Expiry deletes files and rows together - [ ] Delete-everything reports counts for both ### Watch out - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ## Not in this build - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. ## After v1, if you want it - Per-application dictionaries for names and jargon, which is the largest accuracy win available without touching the model - A vocabulary hint sent with each remote request, built from your own corrections rather than from your documents - An offline-only build that removes the remote path entirely, which is a smaller application and a shorter privacy section ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional · secret. Remote transcription only. Unset means the application runs entirely locally. STT_API_KEY=sk-...redacted # Optional. Recorded on every transmission row so history can say where audio went. STT_ENDPOINT_HOST=api.example.com # Required. The local transcription mode. Required, because it is a mode rather than a fallback. LOCAL_MODEL_PATH=~/Models/speech-small.bin # Required. Off by default. When on, files are deleted at their deadline even across restarts. RETAIN_AUDIO=false # Required. Transcripts and their rows are removed together at this age. TRANSCRIPT_RETENTION_DAYS=30 # Required. Remote transcription stops at this figure and local mode takes over. MONTHLY_CEILING=4.00 # Required. Transcripts and the database. Redacted before anything is written here. HISTORY_DIR=~/Library/Application Support/Dictation
You are building a lean indie version of Superscribe. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Superscribe · indie build A dictation tool whose hard problems are capturing only while a key is held without becoming a keyboard observer, keeping an honest record of audio that leaves the machine for a transcription service, and inserting text into another application without stealing or losing the clipboard. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A native macOS menu bar application built with Swift Package Manager | dictation has to be instant and available everywhere, which rules out a web page | | Hotkey | A registered system hotkey, not an event tap observing all input | an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop | | Capture | Audio engine opened on key down and closed on key up, with the state shown | the microphone should be closed whenever you are not holding the key | | Transcription | One streaming provider with a local model as an equal path, chosen per dictation | the choice between the two is a privacy decision the user should make, not a fallback | | Insertion | Paste on release with the previous clipboard restored after a confirmed paste | word-by-word insertion into someone else's text field is where these tools corrupt documents | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running a current macOS, with a second machine or clean user account for permission testing** · free - Why: The permission prompts and secure input behaviour are the interesting part of this build and they only test properly on an account that has not already granted everything. - Get it: Create a fresh user account you can reset. - Verify: A first run on the clean account prompts for microphone access and nothing else until insertion is used - [ ] **A Swift toolchain with Swift Package Manager** · free - Why: Builds the menu bar application without an Xcode project to maintain. - Get it: Install the command line developer tools. - Verify: swift build produces a runnable binary - [ ] **A streaming transcription API key** (optional) · pay per minute of audio; the monthly ceiling in this build is what keeps it bounded - Why: Remote mode is the fast path, and this is the only component that costs money or sends audio anywhere. - Get it: Create a key with a provider whose retention policy you have read, and record what it says. - Verify: A held key produces a transcript and a transmissions row - [ ] **A local speech model you can run on the machine** · free - Why: It is a selectable mode rather than a fallback, and a local path nobody tested is the same as not having one. - Get it: Download a small model and a runtime that can transcribe a wav file offline. - Verify: Local mode produces a transcript with the network disconnected - [ ] **What this tool is allowed to capture and keep, written before the first build** · free - Why: Audio, transcripts and retention windows are much easier to decide now than after a month of dictations exists on the disk. - Get it: Write down whether audio is kept at all, for how long, and what the redaction patterns are. - Verify: The settings match the written policy and the README states it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. - call capture on your existing number (carrier forwarding into a CallKit softphone) - glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables - auto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging - the workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server - Windows parity and signed, notarized auto-updating installers If one of those is essential to you, that is the reason to keep paying for Superscribe, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Superscribe The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a push-to-talk dictation tool that records only while a key is held and says exactly what left the machine to replace Superscribe. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool opens a microphone and puts text into whatever window has focus, which makes two things matter more than any feature. Push to talk, never always listening. Recording starts when the key goes down and stops when it comes up, and there is no mode that opens the microphone and waits for a wake word. macOS shows an orange dot beside the Control Centre icon while the microphone is in use, but Apple's own documentation says only one privacy indicator is shown at a time, so a camera in use replaces it with a green dot. The operating system's indicator is a courtesy, not your permission model: show your own recording state, and capture nothing outside a held key. Second, this is not a keylogger and must not grow into one. Apple's Terminal offers Secure Keyboard Entry specifically to prevent other apps detecting and recording what you type, and advises checking that nothing depends on receiving those keystrokes first. Your hotkey will stop working in that window, and that is correct behaviour rather than a bug to route around. Register a hotkey rather than observing every key, never log keystrokes, and never read the contents of the focused window. And name what leaves. Audio sent to a transcription service is a recording of whatever was in the room, including the other half of a conversation. Record what was sent, to whom, and how long it was, and keep the local transcription path working rather than treating it as a fallback nobody tests. ### Stack (fixed, do not substitute) - Runtime: A native macOS menu bar application built with Swift Package Manager · dictation has to be instant and available everywhere, which rules out a web page - Hotkey: A registered system hotkey, not an event tap observing all input · an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop - Capture: Audio engine opened on key down and closed on key up, with the state shown · the microphone should be closed whenever you are not holding the key - Transcription: One streaming provider with a local model as an equal path, chosen per dictation · the choice between the two is a privacy decision the user should make, not a fallback - Insertion: Paste on release with the previous clipboard restored after a confirmed paste · word-by-word insertion into someone else's text field is where these tools corrupt documents ### Data model (create this before Phase 1) - dictations(id, started_at, ended_at, duration_ms, mode, provider, model_id, transcript_path, cleanup_applied, target_bundle_id, cost_minor) - transmissions(id, dictation_id, provider, endpoint_host, bytes_sent, seconds_sent, retention_note, at) · one row per time audio left the machine, written before the socket opens - audio_files(dictation_id, path, bytes, delete_after) · optional, off by default, and deleted on a timer when on - redactions(pattern, label, added_on) · applied to the transcript before it is stored or shown in history - apps(bundle_id, first_seen_at, insertion_mode, blocked) · per-application behaviour is explicit rather than guessed - settings(hotkey, provider, retain_audio, transcript_retention_days, monthly_ceiling_minor) - There is no keystroke table, no window content and no audio outside the retention setting. A dictations row records that something happened; the transcript itself is optional. ### Phase 1 · A microphone that is shut unless you are holding the key Build: Register a system hotkey rather than installing an input tap. Open the audio engine on key down, close it on key up, and drive a menu bar state from the engine's actual running state rather than from your own flag. Handle the key being released while the window focus changed, the machine sleeping mid-capture, and the audio device disappearing, all by closing the engine. Trap: Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. Done when: The application installs no keyboard event tap, verified by review and by the absence of the corresponding permission prompt on a fresh machine. The menu bar state is derived from the audio engine's running state and a forced engine stop changes it within a stated interval, tested. Sleeping the machine mid-capture closes the engine and discards the partial recording, tested. In a window with secure keyboard entry enabled the hotkey does not fire and the application explains why rather than failing silently. Do not build yet: No transcription, no insertion, no history yet. ### Phase 2 · What left the machine, and when Build: Write the transmissions row before opening the socket, holding the provider, the endpoint host, the seconds and bytes to be sent, and a note of that provider's stated retention. Offer a local model as a selectable mode rather than a fallback, and test both paths in the suite. Show, per dictation in the history, whether audio left the machine and where it went. Trap: Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. Done when: A transmission row exists before any audio leaves, tested by killing the process between the write and the socket opening and finding the row. Local mode produces a transcript with no network activity at all, verified by watching the interface during a test. Each history entry displays the provider and endpoint host or the word local, checked in the interface test. An incomplete transmission row is displayed as uncertain rather than as not sent. Do not build yet: No cleanup pass, no insertion, no retention yet. ### Phase 3 · Getting the text into the other application Build: Run one cleanup pass over the final transcript, apply the redaction patterns, then save the current clipboard, place the text, paste, confirm the paste happened, and restore the previous clipboard only after confirmation. Maintain per-application behaviour in the apps table, and allow an application to be blocked entirely. Never insert word by word while the user is still speaking. Trap: Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. Done when: The previous clipboard is restored only after the paste is confirmed, tested with a deliberately slow target. When confirmation does not arrive within the cap, the transcript stays on the clipboard and the interface says so, tested. A blocked application receives no insertion and the transcript goes to history instead, tested. Nothing is inserted before the key is released, asserted in the suite. Do not build yet: No retention policy, no cost ceiling, no export yet. ### Phase 4 · History is a liability, so treat it like one Build: Apply redaction patterns before a transcript is written to disk, not when it is displayed. Keep audio only when explicitly enabled, and delete it on a timer whether or not the application is running at the time. Expire transcripts on a configured window. Provide a delete-everything action that removes files as well as rows and reports what it removed. Trap: Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. Done when: A dictation containing a redaction pattern leaves no unredacted text anywhere on disk, tested by searching the transcript directory afterwards. Audio is absent unless retention is explicitly enabled, and enabled audio is deleted at its deadline even if the application was closed when it passed, tested. Transcript expiry removes files and rows together, tested. Delete-everything removes files and rows and reports counts for both, tested. Do not build yet: No cost ceiling, no export, no packaging yet. ### Phase 5 · Permissions, cost and a way out Build: On first run, explain each permission the application asks for in one sentence each, and make the application work in local mode with the fewest of them. Track remote transcription spend per month and switch to the local model at the ceiling rather than stopping. Export the history as newline-delimited JSON with the transmission records included. Document what the app cannot see. Trap: Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. Done when: Each permission is requested at first use with a one-sentence explanation, and refusing any one leaves the application usable in a described reduced mode, tested by denying each in turn. Reaching the monthly ceiling switches to local transcription with a visible notice rather than stopping dictation, tested. Export includes dictations and transmissions and reimports into an empty store, tested. The README lists what the application cannot see, and that list is checked against the code by review. Do not build yet: Nothing after this. ### Out of scope (and why) - Any always-listening or wake-word mode, because the entire safety story here rests on the microphone being closed unless a key is held. - Capturing telephone calls on your existing number, which needs carrier forwarding, a softphone and a media pipeline that can only be debugged against live calls, and which brings recording-consent law in with it. - Live word-by-word insertion while speaking, which is where these tools corrupt documents in editors and terminals and is not worth the demo. - Reading the focused window's contents for context, which is a far larger permission than dictation needs and changes what this application is. ### README must contain - That the microphone is open only while the key is held, and that the application shows its own state rather than relying on the system indicator - That the application registers a hotkey and installs no keyboard tap, and that this is why it does not fire in secure input windows - Which provider audio is sent to in remote mode, what that provider says about retention, and where the local record of each transmission lives - That redaction happens before anything is written to disk, and what the transcript and audio retention windows are - What the application cannot see: keystrokes, window contents, and anything outside a held key ===== AGENTS.md ===== # Agent instructions · Superscribe indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A native macOS menu bar application built with Swift Package Manager, A registered system hotkey, not an event tap observing all input, Audio engine opened on key down and closed on key up, with the state shown, One streaming provider with a local model as an equal path, chosen per dictation, Paste on release with the previous clipboard restored after a confirmed paste. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ===== BUILD_PLAN.md ===== # Build plan · Superscribe A dictation tool whose hard problems are capturing only while a key is held without becoming a keyboard observer, keeping an honest record of audio that leaves the machine for a transcription service, and inserting text into another application without stealing or losing the clipboard. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A microphone that is shut unless you are holding the key Capture begins on key down and ends on key up, the application never observes other keystrokes, and the visible state always matches the hardware. ### Steps 1. Register a hotkey, never tap all input The permission you are not asking for is the point. Files: `Sources/Hotkey/Register.swift`, `Tests/NoEventTapTests.swift` 2. Derive the indicator from the engine, not a flag A flag and a microphone can disagree, and the flag is the one that lies. Files: `Sources/Capture/Engine.swift` 3. Close the engine on sleep, focus loss and device change Every one of these has left a microphone open in some shipped app. Files: `Sources/Capture/Lifecycle.swift` ### Done when - [ ] No keyboard event tap is installed - [ ] The indicator follows the engine's real state - [ ] Sleep mid-capture closes the engine and discards audio - [ ] Secure input windows are explained, not worked around ### Watch out - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. ## Phase 2 · What left the machine, and when Every transmission to a remote service is recorded before it happens, and the local path works as well as the remote one. ### Steps 1. Write the transmission row before the socket Afterwards is too late for the only case that matters. Files: `Sources/Transcribe/Remote.swift`, `Tests/TransmissionRecordTests.swift` 2. Make the local model a mode, not a fallback A path nobody tests is a path that does not work. Files: `Sources/Transcribe/Local.swift` 3. Show provenance on every history entry The honest answer to where did that go is in the list. Files: `Sources/UI/HistoryView.swift` ### Done when - [ ] The transmission row precedes the socket - [ ] Local mode produces no network activity - [ ] History shows provider and endpoint or local - [ ] An incomplete transmission reads as uncertain ### Watch out - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. ## Phase 3 · Getting the text into the other application The transcript arrives in the focused application without stealing the clipboard or corrupting a field. ### Steps 1. Confirm the paste before restoring the clipboard A timer-based restore eats the dictation on a busy machine. Files: `Sources/Insert/Paste.swift`, `Tests/ClipboardRestoreTests.swift` 2. Keep per-application behaviour explicit Terminals, editors and browser fields all behave differently. Files: `Sources/Insert/AppPolicy.swift` 3. Never insert before the key is released Live insertion into someone else's document is how text gets mangled. Files: `Sources/Insert/Gate.swift` ### Done when - [ ] Clipboard restore waits for a confirmed paste - [ ] An unconfirmed paste leaves the transcript on the clipboard - [ ] Blocked applications receive no insertion - [ ] Nothing is inserted before key release ### Watch out - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. ## Phase 4 · History is a liability, so treat it like one Transcripts and audio expire, redaction happens before storage, and deleting really deletes. ### Steps 1. Redact before writing, never at display The file is what other software reads. Files: `Sources/Store/Redact.swift`, `Tests/RedactionOnDiskTests.swift` 2. Delete expired audio on next launch as well as on a timer A deadline that passes while the app is closed still passed. Files: `Sources/Store/Retention.swift` 3. Make delete-everything report what it removed A silent delete is indistinguishable from a broken one. Files: `Sources/UI/PrivacyPane.swift` ### Done when - [ ] No unredacted transcript exists on disk - [ ] Audio is absent unless explicitly retained - [ ] Expiry deletes files and rows together - [ ] Delete-everything reports counts for both ### Watch out - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ## Not in this build - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. ## After v1, if you want it - Per-application dictionaries for names and jargon, which is the largest accuracy win available without touching the model - A vocabulary hint sent with each remote request, built from your own corrections rather than from your documents - An offline-only build that removes the remote path entirely, which is a smaller application and a shorter privacy section ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional · secret. Remote transcription only. Unset means the application runs entirely locally. STT_API_KEY=sk-...redacted # Optional. Recorded on every transmission row so history can say where audio went. STT_ENDPOINT_HOST=api.example.com # Required. The local transcription mode. Required, because it is a mode rather than a fallback. LOCAL_MODEL_PATH=~/Models/speech-small.bin # Required. Off by default. When on, files are deleted at their deadline even across restarts. RETAIN_AUDIO=false # Required. Transcripts and their rows are removed together at this age. TRANSCRIPT_RETENTION_DAYS=30 # Required. Remote transcription stops at this figure and local mode takes over. MONTHLY_CEILING=4.00 # Required. Transcripts and the database. Redacted before anything is written here. HISTORY_DIR=~/Library/Application Support/Dictation
You are building a production product version of Superscribe. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Superscribe · product brief ## Problem The desktop dictation half is a solved one-sitting build with complete open-source clones to fork. The hero product is not that: live-transcribing calls on your existing iPhone number takes carrier call forwarding, a pooled Twilio number, a CallKit/PushKit softphone, and a dual-track media-stream pipeline that can only be debugged against live phone calls. And a clone gets you a transcript file, not the workspace around it: roughly 45k lines of API and web app that embed every recording, time block, and project (with synced GitHub repo context), auto-file notes and billable time to the right client, and expose it all as reports, semantic search, a public API, and an MCP server. Each piece is a documented recipe; shipping all of them integrated is the multi-week part even before telephony. ## Product outcome A dictation tool whose microphone is shut unless a key is held, which never observes a keystroke it was not given, which can tell you for any dictation whether audio left the machine and where it went, and which restores your clipboard only once it knows the paste landed. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS microphone + accessibility permissions - streaming STT API key (ElevenLabs Scribe or Deepgram) or local whisper.cpp - LLM API key for transcript cleanup - Twilio account, a number, and an iOS device if you attempt the call-capture half ## Explicit non-goals for v1 - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. - call capture on your existing number (carrier forwarding into a CallKit softphone) - glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables - auto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging - the workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server - Windows parity and signed, notarized auto-updating installers ## Success criteria - No keyboard event tap is confirmed installed - A transmission row is confirmed written before any audio leaves - Local mode is confirmed to produce no network activity - Clipboard restore is confirmed to wait for a confirmed paste - No unredacted transcript is confirmed present on disk ===== BRIEF.md ===== # Build brief · Superscribe The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a push-to-talk dictation tool that records only while a key is held and says exactly what left the machine to replace Superscribe. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool opens a microphone and puts text into whatever window has focus, which makes two things matter more than any feature. Push to talk, never always listening. Recording starts when the key goes down and stops when it comes up, and there is no mode that opens the microphone and waits for a wake word. macOS shows an orange dot beside the Control Centre icon while the microphone is in use, but Apple's own documentation says only one privacy indicator is shown at a time, so a camera in use replaces it with a green dot. The operating system's indicator is a courtesy, not your permission model: show your own recording state, and capture nothing outside a held key. Second, this is not a keylogger and must not grow into one. Apple's Terminal offers Secure Keyboard Entry specifically to prevent other apps detecting and recording what you type, and advises checking that nothing depends on receiving those keystrokes first. Your hotkey will stop working in that window, and that is correct behaviour rather than a bug to route around. Register a hotkey rather than observing every key, never log keystrokes, and never read the contents of the focused window. And name what leaves. Audio sent to a transcription service is a recording of whatever was in the room, including the other half of a conversation. Record what was sent, to whom, and how long it was, and keep the local transcription path working rather than treating it as a fallback nobody tests. ### Stack (fixed, do not substitute) - Runtime: A native macOS menu bar application built with Swift Package Manager · dictation has to be instant and available everywhere, which rules out a web page - Hotkey: A registered system hotkey, not an event tap observing all input · an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop - Capture: Audio engine opened on key down and closed on key up, with the state shown · the microphone should be closed whenever you are not holding the key - Transcription: One streaming provider with a local model as an equal path, chosen per dictation · the choice between the two is a privacy decision the user should make, not a fallback - Insertion: Paste on release with the previous clipboard restored after a confirmed paste · word-by-word insertion into someone else's text field is where these tools corrupt documents ### Data model (create this before Phase 1) - dictations(id, started_at, ended_at, duration_ms, mode, provider, model_id, transcript_path, cleanup_applied, target_bundle_id, cost_minor) - transmissions(id, dictation_id, provider, endpoint_host, bytes_sent, seconds_sent, retention_note, at) · one row per time audio left the machine, written before the socket opens - audio_files(dictation_id, path, bytes, delete_after) · optional, off by default, and deleted on a timer when on - redactions(pattern, label, added_on) · applied to the transcript before it is stored or shown in history - apps(bundle_id, first_seen_at, insertion_mode, blocked) · per-application behaviour is explicit rather than guessed - settings(hotkey, provider, retain_audio, transcript_retention_days, monthly_ceiling_minor) - There is no keystroke table, no window content and no audio outside the retention setting. A dictations row records that something happened; the transcript itself is optional. ### Phase 1 · A microphone that is shut unless you are holding the key Build: Register a system hotkey rather than installing an input tap. Open the audio engine on key down, close it on key up, and drive a menu bar state from the engine's actual running state rather than from your own flag. Handle the key being released while the window focus changed, the machine sleeping mid-capture, and the audio device disappearing, all by closing the engine. Trap: Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. Done when: The application installs no keyboard event tap, verified by review and by the absence of the corresponding permission prompt on a fresh machine. The menu bar state is derived from the audio engine's running state and a forced engine stop changes it within a stated interval, tested. Sleeping the machine mid-capture closes the engine and discards the partial recording, tested. In a window with secure keyboard entry enabled the hotkey does not fire and the application explains why rather than failing silently. Do not build yet: No transcription, no insertion, no history yet. ### Phase 2 · What left the machine, and when Build: Write the transmissions row before opening the socket, holding the provider, the endpoint host, the seconds and bytes to be sent, and a note of that provider's stated retention. Offer a local model as a selectable mode rather than a fallback, and test both paths in the suite. Show, per dictation in the history, whether audio left the machine and where it went. Trap: Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. Done when: A transmission row exists before any audio leaves, tested by killing the process between the write and the socket opening and finding the row. Local mode produces a transcript with no network activity at all, verified by watching the interface during a test. Each history entry displays the provider and endpoint host or the word local, checked in the interface test. An incomplete transmission row is displayed as uncertain rather than as not sent. Do not build yet: No cleanup pass, no insertion, no retention yet. ### Phase 3 · Getting the text into the other application Build: Run one cleanup pass over the final transcript, apply the redaction patterns, then save the current clipboard, place the text, paste, confirm the paste happened, and restore the previous clipboard only after confirmation. Maintain per-application behaviour in the apps table, and allow an application to be blocked entirely. Never insert word by word while the user is still speaking. Trap: Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. Done when: The previous clipboard is restored only after the paste is confirmed, tested with a deliberately slow target. When confirmation does not arrive within the cap, the transcript stays on the clipboard and the interface says so, tested. A blocked application receives no insertion and the transcript goes to history instead, tested. Nothing is inserted before the key is released, asserted in the suite. Do not build yet: No retention policy, no cost ceiling, no export yet. ### Phase 4 · History is a liability, so treat it like one Build: Apply redaction patterns before a transcript is written to disk, not when it is displayed. Keep audio only when explicitly enabled, and delete it on a timer whether or not the application is running at the time. Expire transcripts on a configured window. Provide a delete-everything action that removes files as well as rows and reports what it removed. Trap: Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. Done when: A dictation containing a redaction pattern leaves no unredacted text anywhere on disk, tested by searching the transcript directory afterwards. Audio is absent unless retention is explicitly enabled, and enabled audio is deleted at its deadline even if the application was closed when it passed, tested. Transcript expiry removes files and rows together, tested. Delete-everything removes files and rows and reports counts for both, tested. Do not build yet: No cost ceiling, no export, no packaging yet. ### Phase 5 · Permissions, cost and a way out Build: On first run, explain each permission the application asks for in one sentence each, and make the application work in local mode with the fewest of them. Track remote transcription spend per month and switch to the local model at the ceiling rather than stopping. Export the history as newline-delimited JSON with the transmission records included. Document what the app cannot see. Trap: Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. Done when: Each permission is requested at first use with a one-sentence explanation, and refusing any one leaves the application usable in a described reduced mode, tested by denying each in turn. Reaching the monthly ceiling switches to local transcription with a visible notice rather than stopping dictation, tested. Export includes dictations and transmissions and reimports into an empty store, tested. The README lists what the application cannot see, and that list is checked against the code by review. Do not build yet: Nothing after this. ### Out of scope (and why) - Any always-listening or wake-word mode, because the entire safety story here rests on the microphone being closed unless a key is held. - Capturing telephone calls on your existing number, which needs carrier forwarding, a softphone and a media pipeline that can only be debugged against live calls, and which brings recording-consent law in with it. - Live word-by-word insertion while speaking, which is where these tools corrupt documents in editors and terminals and is not worth the demo. - Reading the focused window's contents for context, which is a far larger permission than dictation needs and changes what this application is. ### README must contain - That the microphone is open only while the key is held, and that the application shows its own state rather than relying on the system indicator - That the application registers a hotkey and installs no keyboard tap, and that this is why it does not fire in secure input windows - Which provider audio is sent to in remote mode, what that provider says about retention, and where the local record of each transmission lives - That redaction happens before anything is written to disk, and what the transcript and audio retention windows are - What the application cannot see: keystrokes, window contents, and anything outside a held key ===== ARCHITECTURE.md ===== # Architecture · Superscribe ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A native macOS menu bar application built with Swift Package Manager | dictation has to be instant and available everywhere, which rules out a web page | | Hotkey | A registered system hotkey, not an event tap observing all input | an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop | | Capture | Audio engine opened on key down and closed on key up, with the state shown | the microphone should be closed whenever you are not holding the key | | Transcription | One streaming provider with a local model as an equal path, chosen per dictation | the choice between the two is a privacy decision the user should make, not a fallback | | Insertion | Paste on release with the previous clipboard restored after a confirmed paste | word-by-word insertion into someone else's text field is where these tools corrupt documents | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Hotkey | Registered hotkey and press state. | Not swappable for an input tap, which is the design decision this build exists around. | | Capture | Engine lifecycle and the visible state. | Any audio path, provided the indicator follows the engine rather than a flag. | | Transcribe | Remote streaming, local model, transmission records. | Any provider, provided the record is written before the socket opens. | | Insert | Clipboard save, paste confirmation and restore. | Any insertion method, provided restore waits for confirmation. | | Store | Redaction before write, retention and deletion. | Any store, provided nothing unredacted reaches the disk. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `STT_API_KEY` · optional, secret · Remote transcription only. Unset means the application runs entirely locally. - `STT_ENDPOINT_HOST` · optional · Recorded on every transmission row so history can say where audio went. - `LOCAL_MODEL_PATH` · required · The local transcription mode. Required, because it is a mode rather than a fallback. - `RETAIN_AUDIO` · required · Off by default. When on, files are deleted at their deadline even across restarts. - `TRANSCRIPT_RETENTION_DAYS` · required · Transcripts and their rows are removed together at this age. - `MONTHLY_CEILING` · required · Remote transcription stops at this figure and local mode takes over. - `HISTORY_DIR` · required · Transcripts and the database. Redacted before anything is written here. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Superscribe product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A native macOS menu bar application built with Swift Package Manager, A registered system hotkey, not an event tap observing all input, Audio engine opened on key down and closed on key up, with the state shown, One streaming provider with a local model as an equal path, chosen per dictation, Paste on release with the previous clipboard restored after a confirmed paste. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. - Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. ===== MILESTONES.md ===== # Delivery milestones · Superscribe Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A microphone that is shut unless you are holding the key Capture begins on key down and ends on key up, the application never observes other keystrokes, and the visible state always matches the hardware. ### Steps 1. Register a hotkey, never tap all input The permission you are not asking for is the point. Files: `Sources/Hotkey/Register.swift`, `Tests/NoEventTapTests.swift` 2. Derive the indicator from the engine, not a flag A flag and a microphone can disagree, and the flag is the one that lies. Files: `Sources/Capture/Engine.swift` 3. Close the engine on sleep, focus loss and device change Every one of these has left a microphone open in some shipped app. Files: `Sources/Capture/Lifecycle.swift` ### Done when - [ ] No keyboard event tap is installed - [ ] The indicator follows the engine's real state - [ ] Sleep mid-capture closes the engine and discards audio - [ ] Secure input windows are explained, not worked around ### Watch out - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. ## M2 · What left the machine, and when Every transmission to a remote service is recorded before it happens, and the local path works as well as the remote one. ### Steps 1. Write the transmission row before the socket Afterwards is too late for the only case that matters. Files: `Sources/Transcribe/Remote.swift`, `Tests/TransmissionRecordTests.swift` 2. Make the local model a mode, not a fallback A path nobody tests is a path that does not work. Files: `Sources/Transcribe/Local.swift` 3. Show provenance on every history entry The honest answer to where did that go is in the list. Files: `Sources/UI/HistoryView.swift` ### Done when - [ ] The transmission row precedes the socket - [ ] Local mode produces no network activity - [ ] History shows provider and endpoint or local - [ ] An incomplete transmission reads as uncertain ### Watch out - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. ## M3 · Getting the text into the other application The transcript arrives in the focused application without stealing the clipboard or corrupting a field. ### Steps 1. Confirm the paste before restoring the clipboard A timer-based restore eats the dictation on a busy machine. Files: `Sources/Insert/Paste.swift`, `Tests/ClipboardRestoreTests.swift` 2. Keep per-application behaviour explicit Terminals, editors and browser fields all behave differently. Files: `Sources/Insert/AppPolicy.swift` 3. Never insert before the key is released Live insertion into someone else's document is how text gets mangled. Files: `Sources/Insert/Gate.swift` ### Done when - [ ] Clipboard restore waits for a confirmed paste - [ ] An unconfirmed paste leaves the transcript on the clipboard - [ ] Blocked applications receive no insertion - [ ] Nothing is inserted before key release ### Watch out - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. ## M4 · History is a liability, so treat it like one Transcripts and audio expire, redaction happens before storage, and deleting really deletes. ### Steps 1. Redact before writing, never at display The file is what other software reads. Files: `Sources/Store/Redact.swift`, `Tests/RedactionOnDiskTests.swift` 2. Delete expired audio on next launch as well as on a timer A deadline that passes while the app is closed still passed. Files: `Sources/Store/Retention.swift` 3. Make delete-everything report what it removed A silent delete is indistinguishable from a broken one. Files: `Sources/UI/PrivacyPane.swift` ### Done when - [ ] No unredacted transcript exists on disk - [ ] Audio is absent unless explicitly retained - [ ] Expiry deletes files and rows together - [ ] Delete-everything reports counts for both ### Watch out - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ## M5 · Permissions, cost and a way out (production only) The permission story is honest, spending has a ceiling, and the history exports in a form something else can read. ### Steps 1. Ask for each permission at first use With one sentence saying why, and a working refusal path. Files: `Sources/Permissions/Request.swift` 2. Degrade to local at the spend ceiling Dictation should get slower, not stop. Files: `Sources/Transcribe/Budget.swift`, `Tests/CeilingTests.swift` 3. Export history with the transmission records The record of what left is part of the history. Files: `Sources/Export/Dump.swift` ### Done when - [ ] Permissions are requested at first use with reasons - [ ] Refusing one leaves a described reduced mode - [ ] The ceiling switches to local rather than stopping - [ ] Export includes transmissions and reimports ### Watch out - Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. ===== OPERATIONS.md ===== # Operations · Superscribe ## Backup Deliberately minimal. Transcripts are a record of things you said in a room and the sensible default is to keep few of them, so the backup story here is the export rather than a nightly copy, and the README should say that plainly. ## Restore Restore the export into an empty store and confirm the dictation count and that every transmission record survived. A restore that loses transmission records loses the only account of what left the machine. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monthly spend against the ceiling, transmission rows without a completion, dictations where the paste was never confirmed, and audio files past their deadline. The second of those is the one that indicates audio left without a full record. ## Incident checklist The incident is audio or text that went somewhere unexpected. The transmissions table, written before the socket opens, is the whole investigation, which is why it is written first and kept longer than the transcripts are. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No keyboard event tap is confirmed installed - [ ] A transmission row is confirmed written before any audio leaves - [ ] Local mode is confirmed to produce no network activity - [ ] Clipboard restore is confirmed to wait for a confirmed paste - [ ] No unredacted transcript is confirmed present on disk ## Launch constraint Do not market omitted Superscribe capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional · secret. Remote transcription only. Unset means the application runs entirely locally. STT_API_KEY=sk-...redacted # Optional. Recorded on every transmission row so history can say where audio went. STT_ENDPOINT_HOST=api.example.com # Required. The local transcription mode. Required, because it is a mode rather than a fallback. LOCAL_MODEL_PATH=~/Models/speech-small.bin # Required. Off by default. When on, files are deleted at their deadline even across restarts. RETAIN_AUDIO=false # Required. Transcripts and their rows are removed together at this age. TRANSCRIPT_RETENTION_DAYS=30 # Required. Remote transcription stops at this figure and local mode takes over. MONTHLY_CEILING=4.00 # Required. Transcripts and the database. Redacted before anything is written here. HISTORY_DIR=~/Library/Application Support/Dictation
# Superscribe · indie build A dictation tool whose hard problems are capturing only while a key is held without becoming a keyboard observer, keeping an honest record of audio that leaves the machine for a transcription service, and inserting text into another application without stealing or losing the clipboard. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A native macOS menu bar application built with Swift Package Manager | dictation has to be instant and available everywhere, which rules out a web page | | Hotkey | A registered system hotkey, not an event tap observing all input | an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop | | Capture | Audio engine opened on key down and closed on key up, with the state shown | the microphone should be closed whenever you are not holding the key | | Transcription | One streaming provider with a local model as an equal path, chosen per dictation | the choice between the two is a privacy decision the user should make, not a fallback | | Insertion | Paste on release with the previous clipboard restored after a confirmed paste | word-by-word insertion into someone else's text field is where these tools corrupt documents | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running a current macOS, with a second machine or clean user account for permission testing** · free - Why: The permission prompts and secure input behaviour are the interesting part of this build and they only test properly on an account that has not already granted everything. - Get it: Create a fresh user account you can reset. - Verify: A first run on the clean account prompts for microphone access and nothing else until insertion is used - [ ] **A Swift toolchain with Swift Package Manager** · free - Why: Builds the menu bar application without an Xcode project to maintain. - Get it: Install the command line developer tools. - Verify: swift build produces a runnable binary - [ ] **A streaming transcription API key** (optional) · pay per minute of audio; the monthly ceiling in this build is what keeps it bounded - Why: Remote mode is the fast path, and this is the only component that costs money or sends audio anywhere. - Get it: Create a key with a provider whose retention policy you have read, and record what it says. - Verify: A held key produces a transcript and a transmissions row - [ ] **A local speech model you can run on the machine** · free - Why: It is a selectable mode rather than a fallback, and a local path nobody tested is the same as not having one. - Get it: Download a small model and a runtime that can transcribe a wav file offline. - Verify: Local mode produces a transcript with the network disconnected - [ ] **What this tool is allowed to capture and keep, written before the first build** · free - Why: Audio, transcripts and retention windows are much easier to decide now than after a month of dictations exists on the disk. - Get it: Write down whether audio is kept at all, for how long, and what the redaction patterns are. - Verify: The settings match the written policy and the README states it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. - call capture on your existing number (carrier forwarding into a CallKit softphone) - glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables - auto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging - the workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server - Windows parity and signed, notarized auto-updating installers If one of those is essential to you, that is the reason to keep paying for Superscribe, and the README should say so rather than pretend.
# Build brief · Superscribe The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a push-to-talk dictation tool that records only while a key is held and says exactly what left the machine to replace Superscribe. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool opens a microphone and puts text into whatever window has focus, which makes two things matter more than any feature. Push to talk, never always listening. Recording starts when the key goes down and stops when it comes up, and there is no mode that opens the microphone and waits for a wake word. macOS shows an orange dot beside the Control Centre icon while the microphone is in use, but Apple's own documentation says only one privacy indicator is shown at a time, so a camera in use replaces it with a green dot. The operating system's indicator is a courtesy, not your permission model: show your own recording state, and capture nothing outside a held key. Second, this is not a keylogger and must not grow into one. Apple's Terminal offers Secure Keyboard Entry specifically to prevent other apps detecting and recording what you type, and advises checking that nothing depends on receiving those keystrokes first. Your hotkey will stop working in that window, and that is correct behaviour rather than a bug to route around. Register a hotkey rather than observing every key, never log keystrokes, and never read the contents of the focused window. And name what leaves. Audio sent to a transcription service is a recording of whatever was in the room, including the other half of a conversation. Record what was sent, to whom, and how long it was, and keep the local transcription path working rather than treating it as a fallback nobody tests. ### Stack (fixed, do not substitute) - Runtime: A native macOS menu bar application built with Swift Package Manager · dictation has to be instant and available everywhere, which rules out a web page - Hotkey: A registered system hotkey, not an event tap observing all input · an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop - Capture: Audio engine opened on key down and closed on key up, with the state shown · the microphone should be closed whenever you are not holding the key - Transcription: One streaming provider with a local model as an equal path, chosen per dictation · the choice between the two is a privacy decision the user should make, not a fallback - Insertion: Paste on release with the previous clipboard restored after a confirmed paste · word-by-word insertion into someone else's text field is where these tools corrupt documents ### Data model (create this before Phase 1) - dictations(id, started_at, ended_at, duration_ms, mode, provider, model_id, transcript_path, cleanup_applied, target_bundle_id, cost_minor) - transmissions(id, dictation_id, provider, endpoint_host, bytes_sent, seconds_sent, retention_note, at) · one row per time audio left the machine, written before the socket opens - audio_files(dictation_id, path, bytes, delete_after) · optional, off by default, and deleted on a timer when on - redactions(pattern, label, added_on) · applied to the transcript before it is stored or shown in history - apps(bundle_id, first_seen_at, insertion_mode, blocked) · per-application behaviour is explicit rather than guessed - settings(hotkey, provider, retain_audio, transcript_retention_days, monthly_ceiling_minor) - There is no keystroke table, no window content and no audio outside the retention setting. A dictations row records that something happened; the transcript itself is optional. ### Phase 1 · A microphone that is shut unless you are holding the key Build: Register a system hotkey rather than installing an input tap. Open the audio engine on key down, close it on key up, and drive a menu bar state from the engine's actual running state rather than from your own flag. Handle the key being released while the window focus changed, the machine sleeping mid-capture, and the audio device disappearing, all by closing the engine. Trap: Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. Done when: The application installs no keyboard event tap, verified by review and by the absence of the corresponding permission prompt on a fresh machine. The menu bar state is derived from the audio engine's running state and a forced engine stop changes it within a stated interval, tested. Sleeping the machine mid-capture closes the engine and discards the partial recording, tested. In a window with secure keyboard entry enabled the hotkey does not fire and the application explains why rather than failing silently. Do not build yet: No transcription, no insertion, no history yet. ### Phase 2 · What left the machine, and when Build: Write the transmissions row before opening the socket, holding the provider, the endpoint host, the seconds and bytes to be sent, and a note of that provider's stated retention. Offer a local model as a selectable mode rather than a fallback, and test both paths in the suite. Show, per dictation in the history, whether audio left the machine and where it went. Trap: Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. Done when: A transmission row exists before any audio leaves, tested by killing the process between the write and the socket opening and finding the row. Local mode produces a transcript with no network activity at all, verified by watching the interface during a test. Each history entry displays the provider and endpoint host or the word local, checked in the interface test. An incomplete transmission row is displayed as uncertain rather than as not sent. Do not build yet: No cleanup pass, no insertion, no retention yet. ### Phase 3 · Getting the text into the other application Build: Run one cleanup pass over the final transcript, apply the redaction patterns, then save the current clipboard, place the text, paste, confirm the paste happened, and restore the previous clipboard only after confirmation. Maintain per-application behaviour in the apps table, and allow an application to be blocked entirely. Never insert word by word while the user is still speaking. Trap: Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. Done when: The previous clipboard is restored only after the paste is confirmed, tested with a deliberately slow target. When confirmation does not arrive within the cap, the transcript stays on the clipboard and the interface says so, tested. A blocked application receives no insertion and the transcript goes to history instead, tested. Nothing is inserted before the key is released, asserted in the suite. Do not build yet: No retention policy, no cost ceiling, no export yet. ### Phase 4 · History is a liability, so treat it like one Build: Apply redaction patterns before a transcript is written to disk, not when it is displayed. Keep audio only when explicitly enabled, and delete it on a timer whether or not the application is running at the time. Expire transcripts on a configured window. Provide a delete-everything action that removes files as well as rows and reports what it removed. Trap: Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. Done when: A dictation containing a redaction pattern leaves no unredacted text anywhere on disk, tested by searching the transcript directory afterwards. Audio is absent unless retention is explicitly enabled, and enabled audio is deleted at its deadline even if the application was closed when it passed, tested. Transcript expiry removes files and rows together, tested. Delete-everything removes files and rows and reports counts for both, tested. Do not build yet: No cost ceiling, no export, no packaging yet. ### Phase 5 · Permissions, cost and a way out Build: On first run, explain each permission the application asks for in one sentence each, and make the application work in local mode with the fewest of them. Track remote transcription spend per month and switch to the local model at the ceiling rather than stopping. Export the history as newline-delimited JSON with the transmission records included. Document what the app cannot see. Trap: Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. Done when: Each permission is requested at first use with a one-sentence explanation, and refusing any one leaves the application usable in a described reduced mode, tested by denying each in turn. Reaching the monthly ceiling switches to local transcription with a visible notice rather than stopping dictation, tested. Export includes dictations and transmissions and reimports into an empty store, tested. The README lists what the application cannot see, and that list is checked against the code by review. Do not build yet: Nothing after this. ### Out of scope (and why) - Any always-listening or wake-word mode, because the entire safety story here rests on the microphone being closed unless a key is held. - Capturing telephone calls on your existing number, which needs carrier forwarding, a softphone and a media pipeline that can only be debugged against live calls, and which brings recording-consent law in with it. - Live word-by-word insertion while speaking, which is where these tools corrupt documents in editors and terminals and is not worth the demo. - Reading the focused window's contents for context, which is a far larger permission than dictation needs and changes what this application is. ### README must contain - That the microphone is open only while the key is held, and that the application shows its own state rather than relying on the system indicator - That the application registers a hotkey and installs no keyboard tap, and that this is why it does not fire in secure input windows - Which provider audio is sent to in remote mode, what that provider says about retention, and where the local record of each transmission lives - That redaction happens before anything is written to disk, and what the transcript and audio retention windows are - What the application cannot see: keystrokes, window contents, and anything outside a held key
# Agent instructions · Superscribe indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A native macOS menu bar application built with Swift Package Manager, A registered system hotkey, not an event tap observing all input, Audio engine opened on key down and closed on key up, with the state shown, One streaming provider with a local model as an equal path, chosen per dictation, Paste on release with the previous clipboard restored after a confirmed paste. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it.
# Build plan · Superscribe A dictation tool whose hard problems are capturing only while a key is held without becoming a keyboard observer, keeping an honest record of audio that leaves the machine for a transcription service, and inserting text into another application without stealing or losing the clipboard. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A microphone that is shut unless you are holding the key Capture begins on key down and ends on key up, the application never observes other keystrokes, and the visible state always matches the hardware. ### Steps 1. Register a hotkey, never tap all input The permission you are not asking for is the point. Files: `Sources/Hotkey/Register.swift`, `Tests/NoEventTapTests.swift` 2. Derive the indicator from the engine, not a flag A flag and a microphone can disagree, and the flag is the one that lies. Files: `Sources/Capture/Engine.swift` 3. Close the engine on sleep, focus loss and device change Every one of these has left a microphone open in some shipped app. Files: `Sources/Capture/Lifecycle.swift` ### Done when - [ ] No keyboard event tap is installed - [ ] The indicator follows the engine's real state - [ ] Sleep mid-capture closes the engine and discards audio - [ ] Secure input windows are explained, not worked around ### Watch out - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. ## Phase 2 · What left the machine, and when Every transmission to a remote service is recorded before it happens, and the local path works as well as the remote one. ### Steps 1. Write the transmission row before the socket Afterwards is too late for the only case that matters. Files: `Sources/Transcribe/Remote.swift`, `Tests/TransmissionRecordTests.swift` 2. Make the local model a mode, not a fallback A path nobody tests is a path that does not work. Files: `Sources/Transcribe/Local.swift` 3. Show provenance on every history entry The honest answer to where did that go is in the list. Files: `Sources/UI/HistoryView.swift` ### Done when - [ ] The transmission row precedes the socket - [ ] Local mode produces no network activity - [ ] History shows provider and endpoint or local - [ ] An incomplete transmission reads as uncertain ### Watch out - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. ## Phase 3 · Getting the text into the other application The transcript arrives in the focused application without stealing the clipboard or corrupting a field. ### Steps 1. Confirm the paste before restoring the clipboard A timer-based restore eats the dictation on a busy machine. Files: `Sources/Insert/Paste.swift`, `Tests/ClipboardRestoreTests.swift` 2. Keep per-application behaviour explicit Terminals, editors and browser fields all behave differently. Files: `Sources/Insert/AppPolicy.swift` 3. Never insert before the key is released Live insertion into someone else's document is how text gets mangled. Files: `Sources/Insert/Gate.swift` ### Done when - [ ] Clipboard restore waits for a confirmed paste - [ ] An unconfirmed paste leaves the transcript on the clipboard - [ ] Blocked applications receive no insertion - [ ] Nothing is inserted before key release ### Watch out - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. ## Phase 4 · History is a liability, so treat it like one Transcripts and audio expire, redaction happens before storage, and deleting really deletes. ### Steps 1. Redact before writing, never at display The file is what other software reads. Files: `Sources/Store/Redact.swift`, `Tests/RedactionOnDiskTests.swift` 2. Delete expired audio on next launch as well as on a timer A deadline that passes while the app is closed still passed. Files: `Sources/Store/Retention.swift` 3. Make delete-everything report what it removed A silent delete is indistinguishable from a broken one. Files: `Sources/UI/PrivacyPane.swift` ### Done when - [ ] No unredacted transcript exists on disk - [ ] Audio is absent unless explicitly retained - [ ] Expiry deletes files and rows together - [ ] Delete-everything reports counts for both ### Watch out - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ## Not in this build - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. ## After v1, if you want it - Per-application dictionaries for names and jargon, which is the largest accuracy win available without touching the model - A vocabulary hint sent with each remote request, built from your own corrections rather than from your documents - An offline-only build that removes the remote path entirely, which is a smaller application and a shorter privacy section
# Copy to .env and fill in. Never commit .env; this file documents it. # Optional · secret. Remote transcription only. Unset means the application runs entirely locally. STT_API_KEY=sk-...redacted # Optional. Recorded on every transmission row so history can say where audio went. STT_ENDPOINT_HOST=api.example.com # Required. The local transcription mode. Required, because it is a mode rather than a fallback. LOCAL_MODEL_PATH=~/Models/speech-small.bin # Required. Off by default. When on, files are deleted at their deadline even across restarts. RETAIN_AUDIO=false # Required. Transcripts and their rows are removed together at this age. TRANSCRIPT_RETENTION_DAYS=30 # Required. Remote transcription stops at this figure and local mode takes over. MONTHLY_CEILING=4.00 # Required. Transcripts and the database. Redacted before anything is written here. HISTORY_DIR=~/Library/Application Support/Dictation
# Superscribe · product brief ## Problem The desktop dictation half is a solved one-sitting build with complete open-source clones to fork. The hero product is not that: live-transcribing calls on your existing iPhone number takes carrier call forwarding, a pooled Twilio number, a CallKit/PushKit softphone, and a dual-track media-stream pipeline that can only be debugged against live phone calls. And a clone gets you a transcript file, not the workspace around it: roughly 45k lines of API and web app that embed every recording, time block, and project (with synced GitHub repo context), auto-file notes and billable time to the right client, and expose it all as reports, semantic search, a public API, and an MCP server. Each piece is a documented recipe; shipping all of them integrated is the multi-week part even before telephony. ## Product outcome A dictation tool whose microphone is shut unless a key is held, which never observes a keystroke it was not given, which can tell you for any dictation whether audio left the machine and where it went, and which restores your clipboard only once it knows the paste landed. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS microphone + accessibility permissions - streaming STT API key (ElevenLabs Scribe or Deepgram) or local whisper.cpp - LLM API key for transcript cleanup - Twilio account, a number, and an iOS device if you attempt the call-capture half ## Explicit non-goals for v1 - Always-listening and wake-word modes. - Telephone call capture on an existing number. - Live word-by-word insertion while speaking. - Reading the focused window's contents. - call capture on your existing number (carrier forwarding into a CallKit softphone) - glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables - auto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging - the workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server - Windows parity and signed, notarized auto-updating installers ## Success criteria - No keyboard event tap is confirmed installed - A transmission row is confirmed written before any audio leaves - Local mode is confirmed to produce no network activity - Clipboard restore is confirmed to wait for a confirmed paste - No unredacted transcript is confirmed present on disk
# Build brief · Superscribe The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a push-to-talk dictation tool that records only while a key is held and says exactly what left the machine to replace Superscribe. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool opens a microphone and puts text into whatever window has focus, which makes two things matter more than any feature. Push to talk, never always listening. Recording starts when the key goes down and stops when it comes up, and there is no mode that opens the microphone and waits for a wake word. macOS shows an orange dot beside the Control Centre icon while the microphone is in use, but Apple's own documentation says only one privacy indicator is shown at a time, so a camera in use replaces it with a green dot. The operating system's indicator is a courtesy, not your permission model: show your own recording state, and capture nothing outside a held key. Second, this is not a keylogger and must not grow into one. Apple's Terminal offers Secure Keyboard Entry specifically to prevent other apps detecting and recording what you type, and advises checking that nothing depends on receiving those keystrokes first. Your hotkey will stop working in that window, and that is correct behaviour rather than a bug to route around. Register a hotkey rather than observing every key, never log keystrokes, and never read the contents of the focused window. And name what leaves. Audio sent to a transcription service is a recording of whatever was in the room, including the other half of a conversation. Record what was sent, to whom, and how long it was, and keep the local transcription path working rather than treating it as a fallback nobody tests. ### Stack (fixed, do not substitute) - Runtime: A native macOS menu bar application built with Swift Package Manager · dictation has to be instant and available everywhere, which rules out a web page - Hotkey: A registered system hotkey, not an event tap observing all input · an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop - Capture: Audio engine opened on key down and closed on key up, with the state shown · the microphone should be closed whenever you are not holding the key - Transcription: One streaming provider with a local model as an equal path, chosen per dictation · the choice between the two is a privacy decision the user should make, not a fallback - Insertion: Paste on release with the previous clipboard restored after a confirmed paste · word-by-word insertion into someone else's text field is where these tools corrupt documents ### Data model (create this before Phase 1) - dictations(id, started_at, ended_at, duration_ms, mode, provider, model_id, transcript_path, cleanup_applied, target_bundle_id, cost_minor) - transmissions(id, dictation_id, provider, endpoint_host, bytes_sent, seconds_sent, retention_note, at) · one row per time audio left the machine, written before the socket opens - audio_files(dictation_id, path, bytes, delete_after) · optional, off by default, and deleted on a timer when on - redactions(pattern, label, added_on) · applied to the transcript before it is stored or shown in history - apps(bundle_id, first_seen_at, insertion_mode, blocked) · per-application behaviour is explicit rather than guessed - settings(hotkey, provider, retain_audio, transcript_retention_days, monthly_ceiling_minor) - There is no keystroke table, no window content and no audio outside the retention setting. A dictations row records that something happened; the transcript itself is optional. ### Phase 1 · A microphone that is shut unless you are holding the key Build: Register a system hotkey rather than installing an input tap. Open the audio engine on key down, close it on key up, and drive a menu bar state from the engine's actual running state rather than from your own flag. Handle the key being released while the window focus changed, the machine sleeping mid-capture, and the audio device disappearing, all by closing the engine. Trap: Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. Done when: The application installs no keyboard event tap, verified by review and by the absence of the corresponding permission prompt on a fresh machine. The menu bar state is derived from the audio engine's running state and a forced engine stop changes it within a stated interval, tested. Sleeping the machine mid-capture closes the engine and discards the partial recording, tested. In a window with secure keyboard entry enabled the hotkey does not fire and the application explains why rather than failing silently. Do not build yet: No transcription, no insertion, no history yet. ### Phase 2 · What left the machine, and when Build: Write the transmissions row before opening the socket, holding the provider, the endpoint host, the seconds and bytes to be sent, and a note of that provider's stated retention. Offer a local model as a selectable mode rather than a fallback, and test both paths in the suite. Show, per dictation in the history, whether audio left the machine and where it went. Trap: Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. Done when: A transmission row exists before any audio leaves, tested by killing the process between the write and the socket opening and finding the row. Local mode produces a transcript with no network activity at all, verified by watching the interface during a test. Each history entry displays the provider and endpoint host or the word local, checked in the interface test. An incomplete transmission row is displayed as uncertain rather than as not sent. Do not build yet: No cleanup pass, no insertion, no retention yet. ### Phase 3 · Getting the text into the other application Build: Run one cleanup pass over the final transcript, apply the redaction patterns, then save the current clipboard, place the text, paste, confirm the paste happened, and restore the previous clipboard only after confirmation. Maintain per-application behaviour in the apps table, and allow an application to be blocked entirely. Never insert word by word while the user is still speaking. Trap: Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. Done when: The previous clipboard is restored only after the paste is confirmed, tested with a deliberately slow target. When confirmation does not arrive within the cap, the transcript stays on the clipboard and the interface says so, tested. A blocked application receives no insertion and the transcript goes to history instead, tested. Nothing is inserted before the key is released, asserted in the suite. Do not build yet: No retention policy, no cost ceiling, no export yet. ### Phase 4 · History is a liability, so treat it like one Build: Apply redaction patterns before a transcript is written to disk, not when it is displayed. Keep audio only when explicitly enabled, and delete it on a timer whether or not the application is running at the time. Expire transcripts on a configured window. Provide a delete-everything action that removes files as well as rows and reports what it removed. Trap: Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. Done when: A dictation containing a redaction pattern leaves no unredacted text anywhere on disk, tested by searching the transcript directory afterwards. Audio is absent unless retention is explicitly enabled, and enabled audio is deleted at its deadline even if the application was closed when it passed, tested. Transcript expiry removes files and rows together, tested. Delete-everything removes files and rows and reports counts for both, tested. Do not build yet: No cost ceiling, no export, no packaging yet. ### Phase 5 · Permissions, cost and a way out Build: On first run, explain each permission the application asks for in one sentence each, and make the application work in local mode with the fewest of them. Track remote transcription spend per month and switch to the local model at the ceiling rather than stopping. Export the history as newline-delimited JSON with the transmission records included. Document what the app cannot see. Trap: Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused. Done when: Each permission is requested at first use with a one-sentence explanation, and refusing any one leaves the application usable in a described reduced mode, tested by denying each in turn. Reaching the monthly ceiling switches to local transcription with a visible notice rather than stopping dictation, tested. Export includes dictations and transmissions and reimports into an empty store, tested. The README lists what the application cannot see, and that list is checked against the code by review. Do not build yet: Nothing after this. ### Out of scope (and why) - Any always-listening or wake-word mode, because the entire safety story here rests on the microphone being closed unless a key is held. - Capturing telephone calls on your existing number, which needs carrier forwarding, a softphone and a media pipeline that can only be debugged against live calls, and which brings recording-consent law in with it. - Live word-by-word insertion while speaking, which is where these tools corrupt documents in editors and terminals and is not worth the demo. - Reading the focused window's contents for context, which is a far larger permission than dictation needs and changes what this application is. ### README must contain - That the microphone is open only while the key is held, and that the application shows its own state rather than relying on the system indicator - That the application registers a hotkey and installs no keyboard tap, and that this is why it does not fire in secure input windows - Which provider audio is sent to in remote mode, what that provider says about retention, and where the local record of each transmission lives - That redaction happens before anything is written to disk, and what the transcript and audio retention windows are - What the application cannot see: keystrokes, window contents, and anything outside a held key
# Architecture · Superscribe ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A native macOS menu bar application built with Swift Package Manager | dictation has to be instant and available everywhere, which rules out a web page | | Hotkey | A registered system hotkey, not an event tap observing all input | an app that watches every keystroke is the thing Secure Keyboard Entry exists to stop | | Capture | Audio engine opened on key down and closed on key up, with the state shown | the microphone should be closed whenever you are not holding the key | | Transcription | One streaming provider with a local model as an equal path, chosen per dictation | the choice between the two is a privacy decision the user should make, not a fallback | | Insertion | Paste on release with the previous clipboard restored after a confirmed paste | word-by-word insertion into someone else's text field is where these tools corrupt documents | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Hotkey | Registered hotkey and press state. | Not swappable for an input tap, which is the design decision this build exists around. | | Capture | Engine lifecycle and the visible state. | Any audio path, provided the indicator follows the engine rather than a flag. | | Transcribe | Remote streaming, local model, transmission records. | Any provider, provided the record is written before the socket opens. | | Insert | Clipboard save, paste confirmation and restore. | Any insertion method, provided restore waits for confirmation. | | Store | Redaction before write, retention and deletion. | Any store, provided nothing unredacted reaches the disk. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `STT_API_KEY` · optional, secret · Remote transcription only. Unset means the application runs entirely locally. - `STT_ENDPOINT_HOST` · optional · Recorded on every transmission row so history can say where audio went. - `LOCAL_MODEL_PATH` · required · The local transcription mode. Required, because it is a mode rather than a fallback. - `RETAIN_AUDIO` · required · Off by default. When on, files are deleted at their deadline even across restarts. - `TRANSCRIPT_RETENTION_DAYS` · required · Transcripts and their rows are removed together at this age. - `MONTHLY_CEILING` · required · Remote transcription stops at this figure and local mode takes over. - `HISTORY_DIR` · required · Transcripts and the database. Redacted before anything is written here. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Superscribe product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A native macOS menu bar application built with Swift Package Manager, A registered system hotkey, not an event tap observing all input, Audio engine opened on key down and closed on key up, with the state shown, One streaming provider with a local model as an equal path, chosen per dictation, Paste on release with the previous clipboard restored after a confirmed paste. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. - Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused.
# Delivery milestones · Superscribe Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A microphone that is shut unless you are holding the key Capture begins on key down and ends on key up, the application never observes other keystrokes, and the visible state always matches the hardware. ### Steps 1. Register a hotkey, never tap all input The permission you are not asking for is the point. Files: `Sources/Hotkey/Register.swift`, `Tests/NoEventTapTests.swift` 2. Derive the indicator from the engine, not a flag A flag and a microphone can disagree, and the flag is the one that lies. Files: `Sources/Capture/Engine.swift` 3. Close the engine on sleep, focus loss and device change Every one of these has left a microphone open in some shipped app. Files: `Sources/Capture/Lifecycle.swift` ### Done when - [ ] No keyboard event tap is installed - [ ] The indicator follows the engine's real state - [ ] Sleep mid-capture closes the engine and discards audio - [ ] Secure input windows are explained, not worked around ### Watch out - Implementing the hotkey with an event tap that watches all keyboard input gives your app the ability to read passwords, requires the permission the operating system grants most reluctantly, and stops working the moment any application enables secure keyboard entry, which you will then be tempted to work around. Register the hotkey through the supported interface, accept that it does not fire inside a secure input window, and say so in the interface rather than retrying. ## M2 · What left the machine, and when Every transmission to a remote service is recorded before it happens, and the local path works as well as the remote one. ### Steps 1. Write the transmission row before the socket Afterwards is too late for the only case that matters. Files: `Sources/Transcribe/Remote.swift`, `Tests/TransmissionRecordTests.swift` 2. Make the local model a mode, not a fallback A path nobody tests is a path that does not work. Files: `Sources/Transcribe/Local.swift` 3. Show provenance on every history entry The honest answer to where did that go is in the list. Files: `Sources/UI/HistoryView.swift` ### Done when - [ ] The transmission row precedes the socket - [ ] Local mode produces no network activity - [ ] History shows provider and endpoint or local - [ ] An incomplete transmission reads as uncertain ### Watch out - Recording the transmission after the call returns means a crash, a cancellation or a network failure leaves audio that went to a third party with no local record of it having gone, which is precisely the case somebody will ask about. Write the row first, mark it complete afterwards, and treat a row without a completion as audio that probably left rather than audio that did not. ## M3 · Getting the text into the other application The transcript arrives in the focused application without stealing the clipboard or corrupting a field. ### Steps 1. Confirm the paste before restoring the clipboard A timer-based restore eats the dictation on a busy machine. Files: `Sources/Insert/Paste.swift`, `Tests/ClipboardRestoreTests.swift` 2. Keep per-application behaviour explicit Terminals, editors and browser fields all behave differently. Files: `Sources/Insert/AppPolicy.swift` 3. Never insert before the key is released Live insertion into someone else's document is how text gets mangled. Files: `Sources/Insert/Gate.swift` ### Done when - [ ] Clipboard restore waits for a confirmed paste - [ ] An unconfirmed paste leaves the transcript on the clipboard - [ ] Blocked applications receive no insertion - [ ] Nothing is inserted before key release ### Watch out - Restoring the clipboard on a fixed timer after issuing the paste means that on a slow or busy application the restore lands first and the user pastes whatever was there before, losing the dictation and looking like the tool dropped it. Confirm the paste landed before restoring, cap the wait, and if confirmation never arrives leave the transcript on the clipboard and say so rather than restoring over it. ## M4 · History is a liability, so treat it like one Transcripts and audio expire, redaction happens before storage, and deleting really deletes. ### Steps 1. Redact before writing, never at display The file is what other software reads. Files: `Sources/Store/Redact.swift`, `Tests/RedactionOnDiskTests.swift` 2. Delete expired audio on next launch as well as on a timer A deadline that passes while the app is closed still passed. Files: `Sources/Store/Retention.swift` 3. Make delete-everything report what it removed A silent delete is indistinguishable from a broken one. Files: `Sources/UI/PrivacyPane.swift` ### Done when - [ ] No unredacted transcript exists on disk - [ ] Audio is absent unless explicitly retained - [ ] Expiry deletes files and rows together - [ ] Delete-everything reports counts for both ### Watch out - Redacting at display time leaves the unredacted transcript sitting in a plain file that any backup, any search index and any other application can read, so the redaction protects the interface rather than the user. Redact before the write, keep no shadow copy of the original, and test by grepping the transcript directory for the pattern after a dictation containing it. ## M5 · Permissions, cost and a way out (production only) The permission story is honest, spending has a ceiling, and the history exports in a form something else can read. ### Steps 1. Ask for each permission at first use With one sentence saying why, and a working refusal path. Files: `Sources/Permissions/Request.swift` 2. Degrade to local at the spend ceiling Dictation should get slower, not stop. Files: `Sources/Transcribe/Budget.swift`, `Tests/CeilingTests.swift` 3. Export history with the transmission records The record of what left is part of the history. Files: `Sources/Export/Dump.swift` ### Done when - [ ] Permissions are requested at first use with reasons - [ ] Refusing one leaves a described reduced mode - [ ] The ceiling switches to local rather than stopping - [ ] Export includes transmissions and reimports ### Watch out - Requesting every permission at launch so the flow is smooth later trains people to grant microphone and automation access without reading, and it is exactly the pattern a tool that types into other applications should not be teaching. Ask at the moment each capability is first used, explain what it is for in one sentence, and keep the application usable when one is refused.
# Operations · Superscribe ## Backup Deliberately minimal. Transcripts are a record of things you said in a room and the sensible default is to keep few of them, so the backup story here is the export rather than a nightly copy, and the README should say that plainly. ## Restore Restore the export into an empty store and confirm the dictation count and that every transmission record survived. A restore that loses transmission records loses the only account of what left the machine. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monthly spend against the ceiling, transmission rows without a completion, dictations where the paste was never confirmed, and audio files past their deadline. The second of those is the one that indicates audio left without a full record. ## Incident checklist The incident is audio or text that went somewhere unexpected. The transmissions table, written before the socket opens, is the whole investigation, which is why it is written first and kept longer than the transcripts are. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No keyboard event tap is confirmed installed - [ ] A transmission row is confirmed written before any audio leaves - [ ] Local mode is confirmed to produce no network activity - [ ] Clipboard restore is confirmed to wait for a confirmed paste - [ ] No unredacted transcript is confirmed present on disk ## Launch constraint Do not market omitted Superscribe capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Optional · secret. Remote transcription only. Unset means the application runs entirely locally. STT_API_KEY=sk-...redacted # Optional. Recorded on every transmission row so history can say where audio went. STT_ENDPOINT_HOST=api.example.com # Required. The local transcription mode. Required, because it is a mode rather than a fallback. LOCAL_MODEL_PATH=~/Models/speech-small.bin # Required. Off by default. When on, files are deleted at their deadline even across restarts. RETAIN_AUDIO=false # Required. Transcripts and their rows are removed together at this age. TRANSCRIPT_RETENTION_DAYS=30 # Required. Remote transcription stops at this figure and local mode takes over. MONTHLY_CEILING=4.00 # Required. Transcripts and the database. Redacted before anything is written here. HISTORY_DIR=~/Library/Application Support/Dictation
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay for the phone rail (answer calls normally on the number they already have, no bot, no second device) and for everything downstream arriving pre-filed: transcripts semantically matched to the right client and project using their own GitHub activity as context, then turned into searchable history, invoices, and CRM drafts.
xcall capture on your existing number (carrier forwarding into a CallKit softphone)
xglitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables
xauto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging
xthe workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server
xWindows parity and signed, notarized auto-updating installers
Superscribe pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| superscribe pro | $18/user | $14.33/user | Desktop dictation on Mac and Windows, AI cleanup, time tracking, templates, project context, and reports |
| business voice | $38/user | $30.40/user | 1+ seats; pooled call minutes, existing-number routing, live transcription, AI summaries, and Pro for every seat |
| travel pass | custom | — | 7 days of Superscribe Phone with included call hours |
free tierNo permanent free tier verified; Pro has a no-card try-now flow, but the official page publishes neither a trial duration nor a numeric usage allowance.
billingPro monthly or $172/year; Business Voice monthly or yearly at a stated 20% discount; Travel Pass is a one-time 7-day purchase
hidden costsBusiness Voice's pooled call-minute quantity and overage treatment are not publicly disclosed. More than 10 seats moves to sales; Travel Pass also hides its included call-hour number.
verified 2026-08-14 · source ↗
Vibecode Superscribe
Kinda. The core of Superscribe is buildable in a weekend with the prompt on this page, but there are real gaps: call capture on your existing number (carrier forwarding into a CallKit softphone), glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables. Read the honest list above before committing.
How much does Superscribe cost?
Superscribe costs about $38/month (Business Voice, checked 2026-07-30), which is $456 per year.
What do I lose by replacing Superscribe?
Honestly: call capture on your existing number (carrier forwarding into a CallKit softphone); glitch-free live word-by-word insertion tuned per app: terminals, Electron editors, browser contenteditables; auto-filing: dictations and calls alike are matched to the right project via embeddings (enriched with GitHub repo context) plus live commit activity during the work block, so billable time lands on the right client without tagging; the workspace around the transcripts: semantic search, invoice-ready PDF reports, CRM note drafts, a public API, and an MCP server; Windows parity and signed, notarized auto-updating installers. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Superscribe?
Yes: Handy (MIT cross-platform push-to-talk dictation app in Tauri, built to be forked; covers the whole desktop core loop.), VoiceInk (GPL native Swift macOS dictation app with local whisper.cpp and per-app modes; closest open clone of the mac side.), FreeFlow (Solo-built open Wispr Flow clone; working example of cloud STT plus LLM cleanup with active-window context.), Twilio Media Streams (Official docs and tutorials for streaming live call audio to STT; the happy path of the call-capture half.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.