I wanted to know how a desktop app's window talks to its main process. In Electron, that is the first question behind almost every "build me a feature like theirs" job: which calls cross the bridge, and what answers them on the other side.
So I handed a shipped app to a reverse-engineering tool and read the summary. Three context bridges. None of them belonged to the app.
The tool everyone starred this week
REA, short for Reverse Engineer Anything, is an MCP server and CLI that lets a coding agent inspect apps it has no source for. Native binaries go through Hopper, Ghidra or IDA. JavaScript and Electron apps, .NET assemblies, Android packages and websites have their own paths. It was #1 on GitHub trending on Sunday with 25,599 new stars in a day (about 118,000 total, MIT licensed), and its Hacker News thread reached 774 points and 327 comments.
The pitch is direct: see a feature you like in someone's app, and have your agent work out how it is built. The npm release, 6.4.0, ships 127 CLI commands (I counted them from --help).
Most of those need a native disassembler. The static JavaScript path does not, so that is the one I ran. I skipped rea setup, which registers REA in your agents' MCP configs, and called the CLI through npx in a scratch folder.
The run
My target was Electron Fiddle 0.39.0, the Electron team's own MIT-licensed playground app. Open source on purpose: I could check every answer against the real code. I downloaded the macOS arm64 zip from its GitHub release, matched its SHA-256 to the digest GitHub publishes, and pulled out only app.asar (21.9 MB). Nothing was installed or launched.
npx -y rea-agents@latest analyze-javascript-application /abs/path/app.asar --json > fiddle.json
About three minutes later I had 43 MB of JSON. REA inventoried 89 files, parsed 71 JavaScript files and visited 2,386,900 syntax nodes without executing a line of the app. The Electron summary, trimmed:
context_bridge_apis: 3 exposed_api_members: 5
ipc.operations: 26 ipc.literal_channels: 11
ipc.dynamic_channel_operations: 6
Every one of those findings is real, and none of them is Fiddle's. The 11 literal channels (clipboard:readText, open-github, get-displays and the rest) come from the example fiddles the app bundles as starter templates, under static/show-me/. The six dynamic channels and the __SENTRY_IPC__ bridge belong to the Sentry SDK. Fiddle's own bridge, window.ElectronFiddle, is not in the counts.
Where the answer went
It was not silently dropped, and that part deserves full credit.
The preload that defines the bridge is a 9,768-byte file. REA parsed it completely, then attached two unknown nodes to it. One says semantic analysis of the file "was not started" because the parent had reserved its remaining result capacity. The other says its relationships exceeded the result budget. The main-process bundle got its own entry: the analyzer worker ran out of its default 1,024 MiB heap there. The graph carries 110 unknown nodes in total, and its coverage field reads partial.
REA works through the files in path order, and the preload was number 23 of 89. Ahead of it sat the 2.9 MB main bundle, the 4.9 MB renderer bundle and Monaco's language workers. The small file holding the answer arrived after the budget was gone. Raising the analyzer heap to 4,096 MiB did not help: the run took about 20 minutes instead of 3, the main bundle ran out of heap again, and every count above came back the same.
REA's docs describe the fix, a --source-path flag that analyzes only the files you name. It lives on the main branch. The npm release I ran answered Unknown flag: --source-path, and to be fair, the install docs warn that main runs ahead of npm.
The one-line experiment
So I made the selection by hand. I copied that one preload into its own folder and analyzed it alone. Ten seconds later: zero bridges, zero IPC operations, and this time the graph's coverage read complete.
The reason sits in the minified file itself. Webpack does not leave require("electron") where the code uses it. It wraps the require in a numbered module and hands the result out through its own loader:
288(E){E.exports=require("electron")}
// ...later in the same file
const E=_(288)
REA's semantic graph did see the reads of ipcRenderer.send and ipcRenderer.invoke. But it marked the binding E as an unknown value, and REA only treats something as Electron's API when it can prove the alias. So E.contextBridge.exposeInMainWorld(...) never counted as a bridge. The Sentry preload it did catch is a tiny separate file that calls require("electron") directly.
I changed one expression in my copy, _(288) to require("electron"), and ran it again:
context_bridge_apis: 1 (ElectronFiddle, 51 members, 0 unknown)
ipc.operations: 48 ipc.dynamic_channel_operations: 48
Fifty-one members is exactly what Fiddle's src/preload/preload.ts defines; I counted them in the source. All 48 channels stay dynamic (e.IpcEvents.FIDDLE_RUN and so on) because the enum values live in another webpack module, and REA labels each one that way.
For contrast, I pointed it at the unbundled source of Electron's archived API demos app. Seventeen seconds, 30 IPC operations over 18 literal channels, and all 12 renderer sends paired with their main-process handlers. On plain code, the static picture is very good. On a production bundle, the gaps open exactly where the bundler inserted its runtime.
I could not find an open issue about following Electron through webpack's loader. I did not file one; this was a read-and-run session only.
Evidence is a lower bound
Here is the rule I would carry into any agent workflow built on a tool like this: a static count is a floor, never a census. "Found 3 bridges" means at least three, with the rest sitting in the unknowns.
REA is designed around that idea. Its graph docs store unavailable values explicitly "so absence of evidence cannot silently become evidence of absence". Every dynamic channel and every inferred send-to-handler pairing in my runs carried its own limitation string, and the top-level result repeats that missing findings do not establish absence.
The weak link is whoever reads the result. An agent that turns context_bridge_apis: 3 into "the app exposes three APIs" has thrown away the most useful part of the answer. If I put an agent on REA to study a feature, I would make it list every unknown that touches that feature before it writes one conclusion. In my run, that list pointed straight at a 9.8 KB file a person finds in seconds with grep exposeInMainWorld.
That is the real shift here. Copying a feature used to start with a guess about how it works. Now it starts with evidence, and evidence comes with a list of what it could not see.
Before you try it
- It moves very fast. Versions 6.2.0, 6.3.0 and 6.4.0 shipped within about 36 hours, and 130 issues were opened on Oct 11 and 12 (UTC) alone. Issue #1387, still open, records 6.3.0 aborting on a large bundle that 6.2.0 finished; the reporter says 6.4.0 fixed it for them. Pin a version for anything you script.
-
Setup edits your agent config. It shows a plan, asks for approval and keeps backups. One HN commenter still called the paste-this-prompt install "the next evolution of installation by
curl | bash". The CLI works without it. - The legal line is yours. REA's disclaimer rules out DRM bypass and copying proprietary code, and notes that licenses and local law may restrict reverse engineering. I picked an MIT app for that reason. Learning how a feature works and shipping someone else's code are different acts.
When your agent reports that an app has none of something, do you make it show you the unknowns, or do you take the zero?
Top comments (0)