The Ghidra pass on Wispr Flow's helper

What Ghidra did in this project, why it did not run at the start, what it proved, and what it changed. Wispr Flow 1.6.1102 on Windows 11. 2026-10-11. Home · Teardown section 7 · Development journey

Short answer

1. Project status

2. Why Ghidra did not run at the start

Wispr Flow is a Windows app, but most of it is not machine code. It is an Electron app: 133 MB of JavaScript in app.asar. Only one 20,228,632-byte file, Wispr Flow Helper.exe, is machine code. Speech capture, the server connection and transcription are all in the JavaScript. So the first pass answered most of the brief without a disassembler. Only the helper's paste rules needed one.

Ghidra did not run on the helper in the first pass for five reasons. Three are REA design rules. One is the harness, and one is my error.

Reason 1 (REA design): REA never installs Ghidra or Java

REA's own documentation states this in three places:

"REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a compiler." (docs/windows-ghidra-p0.md)
"Setup never installs Ghidra, Java, or Python." … "writes only after approval, and never downloads, installs, upgrades, or modifies Ghidra or Java." (docs/installation.md, Ghidra section)
"Extract Ghidra and install the JDK outside REA, then export absolute paths". (docs/installation.md)

The REA skill that I followed gives the same rule to the agent: "REA setup never installs or upgrades Node.js, npm, Homebrew, Java, Ghidra, IDA, JADX, Binwalk, or Unblob. Use existing prerequisites". The documentation does not give the reason for this rule. INFERRED Ghidra is third-party software: the download is 569,649,598 bytes, and it needs its own JDK. An install of that size changes the PC outside REA's own files.

Reason 2 (REA design): REA does not search the disk for Ghidra

REA finds Ghidra only through the environment variable GHIDRA_INSTALL_DIR. rea doctor reported missing_analysis_engine with the message GHIDRA_INSTALL_DIR is not set. But a Ghidra 12.1.4 copy was already on this PC, in the user's tools folder. A different session had put it there on 2026-10-10. REA could not see it, because the variable was not set.

Reason 3 (REA design): the other two engines were not usable here

Reason 4 (harness): the MCP calls stopped on a timeout

In the first pass, the REA MCP calls ran for a long time with no progress, and Claude Code stopped them on its MCP idle timeout:

This is a limit of the harness (Claude Code), not of the model or of REA. Even with a configured Ghidra, a long Ghidra run through Claude Code's MCP connection would have met the same limit. The second pass did not use Claude Code's MCP connection for this reason (see section 4).

Reason 5 (my error): I used the wrong tool and did not check the engines

Rule learned: "Check that the engine exists before you blame the timeout."

3. The role of Ghidra

What Ghidra did

What Ghidra did not do

4. Setup and run

  1. Java. JDK 23.0.2 was already installed at C:\Program Files\Java\jdk-23, with javac. Ghidra 12.1 needs JDK 21 or newer.
  2. Download. ghidra_12.1.4_PUBLIC_20260921.zip, 569,649,598 bytes. Its SHA-256 matched the release note: ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db.
  3. Unzip. I used C:\Windows\System32\tar.exe. It printed many Can't unlink already-existing object: Permission denied lines. The folder already held the copy from 2026-10-10, and a Ghidra process from that session still held its files open. I compared the folder with the zip: 5,219 files, 0 missing, 0 size differences. I did not stop that process, because I had not started it.
  4. Environment. I set GHIDRA_INSTALL_DIR as a permanent user variable. Claude Code's REA MCP server had started before that, so it still cannot see Ghidra. A restart of Claude Code fixes this.
  5. First run failed. rea analyze ... --provider ghidra stopped after 340 s with "code": "provider_timeout", "operation": "list_documents", "timeout_ms": 330000. REA's default startup limit is 330,000 ms (5.5 minutes).
  6. Time limit raised. REA's documentation names the fix, REA_GHIDRA_STARTUP_TIMEOUT_MS. I set it to 10,800,000 ms (3 hours).
  7. One session for all queries. Each REA CLI command imports and analyzes the file again, because REA's Ghidra project is temporary. So I wrote tools/rea_session.mjs. It starts rea --mcp as a child process, keeps one Ghidra session open, and runs requests from a folder queue. It does not go through Claude Code's MCP connection, so the idle timeout does not apply.
  8. Analysis time. open_binary returned in 0.2 s. The first deep request, binary_overview, waited for Ghidra's analysis. Ghidra's log said Total Time 1310 secs, then Analysis succeeded. 1,310 s is about 22 minutes, about 4 times REA's default limit.
  9. Close. I closed the session with close_binary and saved a snapshot of the results: 796,889 bytes, 79 Evidence records. I stopped my driver by its task ID. Its REA server and Ghidra process ended.

Other errors on the way, with their fixes:

5. Method: finding code without names

  1. The usual method is to search for the Windows functions that paste: SendInput, SetClipboardData, SetWindowsHookExW. REA's find_xrefs_to_name returned name_not_found for all three. The helper does not import them by name in its import table.
  2. The helper writes log messages, such as "Pasted successfully using delayed clipboard manager". Ghidra found these as strings. But a search for code that uses the string's address returned an empty list ([]).
  3. The reason: in NativeAOT, a string is a .NET object. The object has an 8-byte type pointer and a 4-byte length, then the characters. The code points at the start of the object, which is 12 bytes (0xC) before the characters.
  4. So I asked REA for references to string address − 12. That found the function behind each log message. Example: the reference at 0x1403256d8 for "Pasted successfully using delayed clipboard manager".
  5. I read each function as pseudocode and followed its calls to the next function.
  6. tools/strs.py writes the string text next to each PTR_DAT_ reference in the pseudocode, so the files can be read without Ghidra.

Rule learned: "A NativeAOT string reference points 12 bytes before the text."

6. What Ghidra proved

PROVEN These rules come from the pseudocode, its constants and its log strings.

  1. Two paste paths.
    • Delayed-render path (first try). The helper registers the text on the clipboard without copying it, then presses the paste keys. When the target app reads the clipboard, Windows sends the helper message 0x305. The helper then 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.
    • Regular path (fallback). The helper uses it if the delayed-render path fails, or if the text has {{TAB}} or {{BREAK}} markers. It saves the clipboard, writes the text, presses the keys, then restores the clipboard.
  2. Formats written. Text, and also HTML Format when htmlText is not empty.
  3. Paste keys. Ctrl+V is the key array 0x560011 (key code 0x11 Ctrl, 0x56 V). Shift+Insert is 0x2d0010 (0x10 Shift, 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 tries a failed key-up again 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, and 50 ms between sections. On the regular path: 500 ms, then the restore.
  6. Restore delay for each app (delayed-render path). The helper compares the start of the process name, upper and lower case equal (comparison mode 5):
    Apps (process name starts with)Restore delayTime limit for the app to read the clipboard
    vncviewer, tvnviewer, mstsc, msrdc, wfica32, vmware-view, rdcman, anydesk, teamviewer (remote-desktop apps)5,000 ms3,000 ms
    OUTLOOK, olk, WINWORD, WhatsApp3,000 ms3,000 ms
    All other apps500 ms1,000 ms
    If the app does not read the clipboard in that time, 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. 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 reports failure cause 2.
    • When the flag windows-key-up-simulation is on, it releases a held Alt (key codes 0x12, 0xA4, 0xA5) or Windows key (0x5B, 0x5C) with a "masking sequence" that uses Ctrl (0x11).
    • If the target window runs at a higher privilege level (for example, as administrator), 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.

7. Inferred and still hidden

INFERRED

STILL HIDDEN

8. Consequences of the Ghidra pass

8.1 On the findings

TopicBefore Ghidra (first pass)After Ghidra
Message protocol between the app and the helper (PasteText, PasteOutcome, PasteBlocked)PROVEN from the JavaScriptNo change. Ghidra did not touch the JavaScript.
Paste keysHIDDEN. Strings such as SendInput and PasteViaClipboard showed that the helper pastes, but not with which keys.PROVEN Ctrl+V, or Shift+Insert in 3 editors when a flag is on.
Key timing and waitsHIDDENPROVEN 50 ms hold, 50 ms after, 150 ms before Tab, 500 ms on the regular path.
Clipboard restore rulesHIDDEN. Only the class names ClipboardRestore and ClipboardRestorePolicy were known.PROVEN 500 / 3,000 / 5,000 ms by app; no restore when the clipboard changed or a flag is on.
How the helper knows the paste workedHIDDENPROVEN the delayed-render clipboard and message 0x305; 1,000 or 3,000 ms time limit.
RefusalsHIDDEN. Only the message name PasteBlocked was known.PROVEN modifier held (cause 2), higher-privilege window (cause 4, UIPI_RESTRICTIONS).
Which branch runs on a real PCHIDDENSTILL HIDDEN. The server flags decide it.

In the same pass, I checked the teardown again and fixed two errors. Ghidra did not find these; a new count and a new read of the bundles found them:

8.2 On flowlite

Behaviorflowlite beforeflowlite after (copies the helper)
Paste keysAlways Ctrl+VShift+Insert in code, cursor, windsurf; Ctrl+V in other apps
Key timingkeyboard.send("ctrl+v"), no holdKeys down, 50 ms, keys up in reverse order, then 50 ms
Held modifier keysNot checkedWaits up to 2 s for Ctrl, Shift, Alt and Windows to be released
Restore delayFixed 0.15 s0.5 s; 3 s for Outlook, Word, WhatsApp; 5 s for remote-desktop apps
Restore when the clipboard changedAlways restoredSkipped
Restore blocks the next dictationYes (time.sleep)No (threading.Timer)
Log line3.2s audio, 0.85s ASR: '...'5.1s audio, 0.92s ASR, python -> ctrl+v, restore 0.5s: "..."

One difference: Wispr uses Shift+Insert only when its shift-insert flag is on. flowlite has no flags, so it always uses Shift+Insert in those three editors.

Not copied, to keep flowlite small: the delayed-render clipboard, the HTML Format copy, the snapshot of every clipboard format, and the {{TAB}} and {{BREAK}} markers. So flowlite cannot know if a paste reached the app, and it restores only clipboard text. An image on the clipboard is lost.

8.3 Costs

8.4 Changes on this PC

8.5 Changes in the repository

9. The flowlite rebuild and its tests

flowlite is a Python script, so it has no build step. "Rebuilt" means that I changed flowlite/flowlite.py and ran the tests again, in the same commit, 0f18137.

New parts of the code:

Test results:

TestResultWhat it covers
python flowlite.py --selftestselftest okThe key choice for each app, and the 0.5 s, 3 s and 5 s delays. This is the only check of the Shift+Insert branch and of the 3 s and 5 s delays.
python flowlite.py --test test.wavLet's meet at 6 p.m. on Thursday to review the quarterly report.Transcription only
flowlite/receipts/driver.py, inject modeforeground_is_target: true; the test window got "Let's meet at 6 p.m. on Thursday to review the quarterly report. "; clipboard_restored: true; log 5.1s audio, 0.92s ASR, python -> ctrl+v, restore 0.5sThe full path from audio to pasted text. The test window is a python process, so only the Ctrl+V branch ran.
Live voiceNot done—

10. Open items

11. The 21 functions

Each name is INFERRED from the log strings in the function. Each link opens the pseudocode file on GitHub.

Name (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

12. Where each fact is recorded

FactThis pageTeardown §7Journey §17Index
Why no engine ran at first§2 (full)§1 (short)yesno
Setup, time limit, rea_session.mjs§4yesyesno
1,310 s analysis, 56,803 functions§3, §4yes1,310 s onlyno
12-byte string method§5yesyesno
Proven paste rules§6yessummaryas flowlite limits
Inferred and hidden§7yesnono
Consequences (before and after)§8nonono
flowlite changes and tests§9noyesrun steps and limits
Functions with Evidence IDs§11yesnono

Sources for section 2: REA 6.3.0 repository documents docs/installation.md, docs/windows-ghidra-p0.md and docs/ghidra-nativeaot.md, and the REA skill reverse-engineer-anything.