← Index

Wispr Flow 1.6.1102 — how it works on Windows

Static analysis of the installed app files, 2026-10-11. Target: C:\Users\<user>\AppData\Local\WisprFlow\app-1.6.1102\. Background: wisprflow.ai home page and wisprflow.ai/privacy. No part of the app was run.

PROVEN I saw it in the shipped files. INFERRED The names or structure point to it, but I did not see the code do it. HIDDEN The answer is on Wispr's servers or in parts I could not open.

Secrets: the app bundle contains an API key literal for the inference host, a Supabase anonymous token, a PostHog key, and a Sentry DSN. This report names them but does not copy their values.

1. Where the app is and what I inspected

Wispr Flow is not in your Desktop folder. Your Desktop (C:\Users\<user>\Desktop, plus the OneDrive Desktop) has no Wispr file or shortcut. The app is installed by Squirrel at C:\Users\<user>\AppData\Local\WisprFlow\, with three versions: app-1.4.303, app-1.4.338, app-1.6.1102. I analyzed 1.6.1102, the newest. User data is in C:\Users\<user>\AppData\Roaming\Wispr Flow\ (not opened). The project folder holds only a README.txt with this prompt and a link to the video “How To Use REA To Clone Any Software In Minutes” by Eric Tech; I did not watch the video.

PartWhat it isSize
Wispr Flow.exeElectron shell235,430,936 B
resources\app.asarAll JavaScript: .webpack/main/index.js (9,771,276 B main process) and renderer windows hub, status, overlay, scratchpad, meeting_recorder and others. 902 entries, no node_modules folder (everything is bundled).133,651,466 B
resources\Release\Wispr Flow Helper.exeNative Windows helper. .NET compiled ahead of time (NativeAOT): it has System.Private.CoreLib and Wispr Flow Helper.dll strings but no CLR header. Signed by CN="Wispr AI, Inc.", San Francisco.20,228,632 B
resources\migrations\*.js169 SQLite schema migrations for the local flow.sqlite history databasesmall
resources\ax-inspect*.mjsDeveloper tools that render saved accessibility (AX) context of past dictations from flow.sqlite into HTML~104 KB
app.asar.unpacked\...\jabra-device-connector, node_sqlite3.node, crypt32-x64.nodeHeadset buttons (Jabra), local database, Windows certificate store—

How REA was used, and where it failed

Both REA MCP calls failed. analyze_javascript_application on the 133 MB ASAR sent no progress for 3,385 s and inspect_managed_artifact on the helper sent none for 3,666 s; Claude Code then stopped them on its MCP idle timeout. This is a harness limit (the MCP idle timeout), not a model error. inspect_managed_artifact was also the wrong tool: the helper has no CLR header, so it is not a managed PE.

The results below come from four sources: (1) I extracted the ASAR with a small Node script and read the bundles directly; (2) I scanned the helper's ASCII and UTF-16 strings; (3) I ran the REA CLI on the 22 extracted main-process bundles (result in section 6); (4) I ran REA with the Ghidra 12.1.4 engine on the helper and read its paste code as pseudocode (section 7).

Which REA engine did what: the JavaScript part needs no disassembler. REA reads it with its own analyzer, analyze_javascript_application. Only the helper is machine code. In the first pass, no disassembler was set up: rea doctor reported Ghidra, IDA and Hopper as missing_analysis_engine. I then installed Ghidra 12.1.4 and used it for section 7. IDA and Hopper are still not set up.

2. The pipeline, step by step

A. Hotkey. The helper owns the global shortcut. PROVEN main sends UpdateShortcuts, DictationStart, DictationStop to the helper; the helper contains SetWindowsHookExW and LowLevelKeyboardProc (a low-level keyboard hook) and sends KeypressEvent{eventType,key,index,inputType} back. Feature flags windows-default-shortcut and windows-caps-lock-shortcut exist. HIDDEN the default key combination is not a constant I found.
B. Microphone. PROVEN The hub renderer window calls navigator.mediaDevices.getUserMedia, builds an AudioContext at sampleRate: 16000 (hub@744855), and loads dist/recorderWorklet.js. That AudioWorklet buffers 640 samples per message (40 ms at 16 kHz); handleRecorderWorkletMessage (hub@747338) receives them.
C. Encode. PROVEN The renderer encodes with WebCodecs AudioEncoder, config {codec:"opus", sampleRate:16000, numberOfChannels:1, bitrate:64000}, 20 ms packets. If Opus is not supported, it sends WAV (float → Int16 PCM). Each packet goes to the main process over Electron IPC channel dictation:audioChunk; the end goes over dictation:recordingDone. Silent chunks before the first real audio are dropped.
D. Context capture. PROVEN For each dictation, the app collects: app name, bundle id, URL, app type; the focused text box (contents, before_text, selected_text, after_text) through the helper's GetTextBoxInfo; words read from the screen through accessibility (screenContentAx) and through OCR (screenContentOcr); VS Code variable names and file names; nearby text and an accessibility HTML snapshot (contentHtml). The helper contains IUIAutomation, IUIAutomationTextPattern, IUIAutomationValuePattern, IUIAutomationLegacyIAccessiblePattern. HIDDEN which OCR engine runs on Windows (no Windows.Media.Ocr string found).
E. Stream to the server. PROVEN The main process opens a bidirectional gRPC stream flow_api.v1.TranscriptionService/TranscribeStream (protobuf-ts over a grpc-node client, TLS; the bundle does not name the client library, but it contains makeBidiStreamRequest and grpc-node.retry_max_attempts_limit). Each Request carries some of: init (metadata + preferences), context, payload (Opus or WAV packets, max 18,000 per message), commit. Headers: authorization: Bearer <user token>, baseten-authorization: Api-Key <embedded key>, baseten-model-id, x-baseten-environment, disable-formatting. Keepalive pings and a heartbeat timeout run during the stream.
F. Get text back. PROVEN The server streams State messages (raw_text, formatted_text, progress of transcriber and formatter, candidate_beams, audio_snr, audio_volume) and a final Result (output.plaintext, output.html, languages, confidence, response_time, transcript_origin, format_skip_reason). If the heartbeat times out or gRPC fails, main sends the whole recording in one unary gRPC call Transcribe (the code calls it the "Baseten HTTP fallback") and uses whichever answer arrives first.
G. Insert text. PROVEN main reads the clipboard (clipboard.readText()) and saves it, then sends PasteText{text, htmlText?, pasteId, captureKeystrokeTrace?} to the helper. The helper replies PasteOutcome{pasteId, success, timeElapsedMs} or PasteBlocked{reason}, and later DictatedTextVerification{pasteId, verdict, reason, focusedElementContents}. The helper contains PasteViaClipboard, ClipboardSnapshot, ClipboardRestore, ClipboardRestorePolicy, SetClipboardData, HTML Format, SendInput, ShiftInsert. PROVEN by Ghidra (section 7): the helper writes the text (and HTML Format when present) to the clipboard, presses Ctrl+V, or Shift+Insert in VS Code, Cursor and Windsurf when a flag is on, then restores the old clipboard after 500 ms, 3,000 ms or 5,000 ms, depending on the app.
H. Learn from edits. PROVEN After the paste, the helper watches the field (UpdateEditedText, contentObservationEndLastKeystroke) and the app stores edit distance and correction counts in flow.sqlite (migrations 20260409120000-add-correction-counts-to-history.js, 20260717120000-add-history-edit-distance.js).

Servers named in the code

HostRoleStatus
inference.wisprflow.comDefault gRPC dictation targetPROVEN
model-v31pl413.grpc.api.baseten.coSame service hosted on Baseten (model id v31pl413)PROVEN in the allowlist
wispr-ai-prod--prod-grpc-server-flowgrpcserver.us-east.modal.directSame service hosted on ModalPROVEN in the allowlist
https://inference-info.wisprflow.com/dictation_url.txtRemote switch: the app fetches it and changes target only to one of the three hosts abovePROVEN
api.wisprflow.ai, api-east.wisprflow.ai (https and wss)Account and app API; WebSocket constants existINFERRED not traced into dictation
dodjkfqhwrzqjwkfnthl.supabase.co, us.i.posthog.com, ph.wisprflow.ai, SentryBackend data, product analytics, crash reportsPROVEN constants

3. Proven, inferred, hidden

PROVEN from the app files

INFERRED from names and structure

HIDDEN (on the server or not opened)

4. A small version you can build

This copies the proven mechanism in its smallest form: one Python file, one hotkey, one recognizer, the clipboard, and a Ctrl+V keystroke. It skips context, streaming and verification.

Wispr partSmall version
Helper keyboard hookkeyboard package (hold F9 to talk)
getUserMedia 16 kHz monosounddevice, 16 kHz, mono, int16
gRPC stream to cloud ASRLocal: faster-whisper on your CUDA GPU (you already use it in the intake pipeline). Cloud option: any speech-to-text API that takes a WAV file.
Server formatterOptional: one LLM call with "clean up this dictation, keep meaning". Off by default.
Helper PasteTextpyperclip save → set text → keyboard.send("ctrl+v") → wait → restore. The current flowlite/flowlite.py adds the helper rules from section 7: the key combination for each app, the 50 ms key timing, the restore delay for each app, and no restore when the clipboard changed.
# flowlite.py - hold F9, speak, release: text is pasted into the focused app.
# pip install faster-whisper sounddevice numpy keyboard pyperclip
import time, numpy as np, sounddevice as sd, keyboard, pyperclip
from faster_whisper import WhisperModel

RATE, KEY = 16000, "f9"
model = WhisperModel("base.en", device="cuda", compute_type="float16")  # ponytail: "cpu"/"int8" if no GPU

def record():
    chunks = []
    with sd.InputStream(samplerate=RATE, channels=1, dtype="float32",
                        callback=lambda d, *_: chunks.append(d.copy())):
        while keyboard.is_pressed(KEY):
            time.sleep(0.02)
    return np.concatenate(chunks)[:, 0] if chunks else np.zeros(0, np.float32)

def paste(text):
    old = pyperclip.paste()
    pyperclip.copy(text)
    keyboard.send("ctrl+v")
    time.sleep(0.15)          # ponytail: fixed delay; slow apps may paste the old clipboard
    pyperclip.copy(old)       # ponytail: text only; images/HTML on the clipboard are lost

while True:
    keyboard.wait(KEY)
    audio = record()
    if len(audio) < RATE // 4:
        continue              # under 0.25 s: ignore taps
    segs, _ = model.transcribe(audio, vad_filter=True)
    text = " ".join(s.text.strip() for s in segs).strip()
    if text:
        paste(text + " ")

Services it needs

Known limits of this version

Next steps, in order: (1) add a dictionary as faster-whisper initial_prompt; (2) add the LLM cleanup; (3) read the focused text box with uiautomation to verify the paste.

5. Not done

6. REA CLI result

Command: npx -y rea-agents@6.3.0 analyze-javascript-application <22 extracted main bundles + package.json> --json. It finished with exit 0 in about 7 minutes and wrote a 505,348,651-byte Evidence file.

Limit: the graph does not keep string values. The strings dictation:audioChunk, Wispr Flow Helper.exe and inference.wisprflow.com each have 0 matches in the Evidence file. REA also found 0 child-process and 0 request relations, but its own limitations say "Graph coverage is incomplete; unavailable facts remain explicit and must not be read as absence." So the helper launch (spawn) and the gRPC hosts in sections 2–3 come from my direct reading of the bundle text, not from REA.

7. The helper's paste code, read with Ghidra

The full account, with the reasons Ghidra did not run at the start and the consequences of this pass, is on the Ghidra pass page.

Setup. REA 6.3.0 with the Ghidra 12.1.4 engine (JDK 23.0.2) on Wispr Flow Helper.exe, 20,228,632 B, SHA-256 c58c61b106c83f7b77efc921b0e849bb7210154898504215f17306c1a6d6abe0. Ghidra found 56,803 functions and 14,969 strings (Evidence ev_63ff9ab0f8afa4dfd337adbc0aea57e776623c5a8e10cf35790ae26bea871bab). Its analysis took 1,310 s. REA's default Ghidra time limit is 330,000 ms, so the first run failed with provider_timeout. I raised REA_GHIDRA_STARTUP_TIMEOUT_MS to 10,800,000 and kept one REA session open with tools/rea_session.mjs.

Method. NativeAOT removes function names. REA's find_xrefs_to_name returned name_not_found for SendInput, SetClipboardData and SetWindowsHookExW, because the helper does not import them by name in its import table. The helper keeps its log messages as .NET string objects, and the code points at the object, which starts 12 bytes (0xC) before the characters. So I asked REA for references to string address − 12. That found the function behind each log message. Then I read each function as Ghidra pseudocode. tools/strs.py writes the string text next to each reference.

PROVEN paste rules (from pseudocode, constants and log strings)

  1. Two paste paths. First the helper tries a delayed-render clipboard: it registers the text without copying it, then presses the paste keys. When the target app reads the clipboard, Windows sends the helper message 0x305. Then the helper supplies the text as format 13, format 1 or the HTML format, and logs "Clipboard read detected! Rendering delayed data...". This is how the helper knows that the paste reached the app. If that path fails, or the text has {{TAB}}/{{BREAK}} markers, it uses the regular path: save the clipboard, write the text, press the keys, restore.
  2. Formats written. Text, and also HTML Format when htmlText is not empty.
  3. Key combination. Ctrl+V is the key array 0x560011 (VK 0x11 Ctrl, VK 0x56 V). Shift+Insert is 0x2d0010 (VK 0x10 Shift, VK 0x2D Insert). The helper uses Shift+Insert only when the flag shift-insert is on and the focused process is Cursor, Windsurf or Code. Otherwise it uses Ctrl+V.
  4. Key timing. It sends all key-downs in one SendInput call, waits 50 ms (Sleep(0x32)), then sends all key-ups. It retries a failed key-up once.
  5. Waits. 50 ms after the keys ("Pasted text in window, now sleeping for 50 ms for paste to complete"). In marked text: 150 ms before a Tab key, 50 ms between sections. On the regular path: 500 ms, then restore.
  6. Restore delay per app (delayed path). The helper compares the start of the process name with comparison mode 5 (in .NET that is OrdinalIgnoreCase, so case does not matter; the mode name is from my knowledge of .NET):
    • 5,000 ms: vncviewer, tvnviewer, mstsc, msrdc, wfica32, vmware-view, rdcman, anydesk, teamviewer (remote-desktop apps).
    • 3,000 ms: OUTLOOK, olk, WINWORD, WhatsApp.
    • 500 ms: all other apps.
    If the app does not read the clipboard in time (3,000 ms for the two lists above, 1,000 ms for other apps), the helper logs "Clipboard not read within timeout -- paste likely failed" and reports a failure.
  7. No restore when the clipboard changed after the paste ("Clipboard changed since paste, skipping restoration") or when the flag disable-clipboard-restore is on.
  8. What is saved and restored. The helper keeps two lists of clipboard formats. FormatsToRestore: Text, UnicodeText, HTML Format, the bitmap and image formats, and the Windows clipboard-history markers (CanIncludeInClipboardHistory, CanUploadToCloudClipboard, ExcludeClipboardContentFromMonitorProcessing, Clipboard Viewer Ignore). FormatsToIgnore: FileDrop, Rich Text Format, Portable Document Format, WaveAudio, Csv and others. Any other format is logged as "Found unknown clipboard format".
  9. Safety checks. If a modifier key is held, the helper logs failure cause 2. When the flag windows-key-up-simulation is on, it releases a held Alt (VK 0x12, 0xA4, 0xA5) or Windows key (VK 0x5B, 0x5C) with a "masking sequence" that uses VK 0x11 (Ctrl). If the target window runs at a higher privilege level, the paste fails with cause 4, UIPI_RESTRICTIONS. If the keys were already sent, it does not paste again on the regular path, so the text cannot appear twice.
  10. Threading. The regular path runs on an STA thread. If the caller is not on one, the helper starts a new STA thread and waits for it.

INFERRED

STILL HIDDEN

Functions read (pseudocode in ghidra/pseudocode/)

Label (inferred)Ghidra functionREA Evidence ID
PasteTextFUN_140325210ev_dcadbc2da87cdcb9accca51a40f30008d8d196253852aa77184a1638cccaa035
PasteTextInternalFUN_140325ec0ev_5c80d4f5a8f7c23b962cff65491a5e63fb84f67b399c62c0720dee817050a0f8
PasteRegularFUN_1403259d0ev_3107c101e2232fe2554dfcc240e802a5558dd60f3fea6860fb11c20c9e2487af
SimulatePasteFUN_140324cc0ev_1e82ad9e36f4d70d5a77d3d00242672b8fbf172a0052f21f82ca4582d4d60620
SendMultipleKeyEventsFUN_140322420ev_e4cf49c4216da88aec83106d51c761f9adf73d1e071f9e55c2eb2a2eabccbe23
SendMaskingSequenceFUN_140323170ev_0b7f777d818d6fe2903427fc9f6443018cd48532f5ee8e191b008ddb6fa75d70
SimulateTabFUN_140325720ev_fa8232b713df8387028f5ca726685cdb5e1d1f876825cd623a26474b0259991f
SleepFUN_14016a3e0ev_a6c8afed1703f254b9ac1f108d1612d31b0bbea3614d471ff39f8ba8f9b4c19c
CopyWithFormattingFUN_140326e60ev_8dc21b58ac38790f87852027f56292372a67e0787af469efc26e6cb35c2c6395
RestoreAfterPasteFUN_140326b76ev_d010683ff6d1d581c03156211d5146324e007f004bfeac7bb7dd8dbf8ed81311
RestoreDelayMsFUN_140318380ev_0a153dd50f3f4cdf8c2a0a51144ffa243c9091b31399482590a78bb3e1207250
FailureTimeoutMsFUN_1403183f0ev_9caad8a7122a8f59c1e9718e8fc9db0387bf82c6e5ed019983639c6bfd572b2d
ProcessPrefixMatchFUN_140318320ev_db2a095a44aa4e31510f8a32a5da2e0f271ffb1094e32c6cb7e7a8ce5e071893
RestoreDelayAppListsFUN_140318450ev_953a27683c90b4a9fa3f3796ac1966cbea44565e888a45efead90f64fc84ca01
ForegroundAppInitFUN_140409240ev_62474b466f60f64b3f6de80945d4f4bc7ac6681ebda6c61d262fa9dad7b55600
CapturePasteTargetFUN_1403196b0ev_2e36ae139aa1308f38ee98ac2eb7b38e5ccdb8aa4dceff278c8a3dfa210f2e30
LoadDelayedClipboardFUN_1403186e0ev_d0d08a755fa52ab537fdde28e664d4cfeff4d52b61ce876f16b8ad5c18893605
DelayedRenderWndProcFUN_140319890ev_485e2d7d7c324d5656a3de994bd8d7f1e2c7e2303b167b8d827e1a5150bf1198
ScheduleRestoreFUN_140319cf0ev_0d0ff5eb9ff59fc4ce85c2e039072540d5ff7d2949101ee95163098ed30f76d3
ClipboardReadTimeoutFUN_140411990ev_f382f1ce023ca79de30a20a4f5905b4c72e3ccc5932d5f581d63a3ef559b6545
ClipboardFormatListsFUN_140327960ev_7ccd18a16a303acd80a19274017a1575d560de2869a3ac246a297bf4c2fa9aaf

Evidence locations

Offsets are character positions in the extracted files under scratchpad\asar_out\.webpack\.

ClaimFile @ offset
Recorder worklet, 640-sample chunksrenderer/dist/recorderWorklet.js
16 kHz, Opus 64 kbps configrenderer/hub/index.js@665196
Dictation audio graphrenderer/hub/index.js@744855
Opus encoder → dictation:audioChunkrenderer/hub/index.js@758524, channel name main/index.js@2692782
Endpoint constantsmain/index.js@2632015
Dictation host allowlist, dictation_url.txtmain/index.js@3589039
TranscriptionService definitionmain/index.js@3692633
gRPC client, headers, context, fallbackmain/index.js 3,694,000–3,740,000
Helper spawn (stdio pipes, fd 3 IPC)main/index.js@4435889
Clipboard save + PasteTextmain/index.js@4465164
Helper JSON schema (PasteTextPayload, TextBoxInfoPayload)main/index.js ~2,492,000