Skip to content

Tutorial

Reverse engineering Electron apps with REA

Find an Electron app's app.asar, map its preload APIs and IPC channels with REA, trace a feature from renderer to main process, and observe the running app.

Commands verified 2026-10-12 · rea-agents 6.4.0 · macOS, Node.js 22

Electron apps — Slack, Notion, Obsidian, VS Code and thousands more — ship their application code as JavaScript. That makes them some of the most rewarding targets to reverse engineer: no disassembler required, just a way to find your feature in megabytes of minified bundles. REA does the mapping; you and your agent do the reading.

How an Electron app is built

Almost every interesting feature crosses Electron’s process boundary, so it helps to know the three layers:

  • Renderer — the web page you see. It usually has no direct Node.js access.
  • Preload — a script that exposes a small API to the page with contextBridge.exposeInMainWorld.
  • Main process — full Node.js. It receives requests through ipcMain.handle and touches files, the clipboard, the network.

The thread connecting them is an IPC channel name — a string like notes:export. Find it in the preload, find the same string in the main process, and you have the feature’s route. REA’s static analysis pairs these for you.

1. Find app.asar

Packaged apps keep their code in an ASAR archive inside the resources folder:

OSTypical location
macOS/Applications/AppName.app/Contents/Resources/app.asar
Windowsresources\app.asar next to the app’s .exe
Linuxresources/app.asar in the install directory

You don’t need to extract it — REA reads the archive directly and reports paths inside it. If you want to browse the files yourself, Electron’s official tool unpacks it:

shell
npx @electron/asar extract /absolute/path/to/app.asar ./app-src

2. Map the app statically

Static analysis reads the folder or ASAR without running a line of the app’s code and needs only Node.js — no Hopper, Ghidra or Electron install:

shell
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app.asar --json > app-evidence.json

Practice on REA’s own notes-example first. Running the command above against it on rea-agents 6.4.0 produced this normalized_result.summary.ipc:

json
{
  "operations": 2,
  "literal_channels": 1,
  "dynamic_channel_operations": 0,
  "renderer_transmissions": 1,
  "main_handlers": 1,
  "paired_renderer_transmissions": 1,
  "ambiguous_renderer_transmissions": 0,
  "unpaired_literal_renderer_transmissions": 0
}

One literal channel, one sender, one handler, paired. The same summary also lists preload_entrypoints, context_bridge_apis, exposed_api_members, sender_validation_observations and native_addon_bindings — a table of contents for the whole app. Source locations live in normalized_result.graph.

Real apps produce big files — REA’s docs cite a 334 MB evidence file for Obsidian. Project a summary instead of reading it whole:

shell
jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' app-evidence.json > app-view.json
npx -y rea-agents@latest inspect-analysis-view app-view.json

3. Trace one feature

Pick a literal you can see in the app — a button label, an API name, an IPC channel — and trace everything reachable from it. The CLI takes the saved evidence plus a seed:

shell
node --input-type=module -e '
import { readFileSync, writeFileSync } from "node:fs";
const application = JSON.parse(readFileSync("app-evidence.json", "utf8"));
writeFileSync("trace-input.json", JSON.stringify({
  application, seed: { kind: "string", value: "notes:export" }, direction: "both"
}));'
npx -y rea-agents@latest trace-application-feature ./trace-input.json --json > trace.json

On the notes-example this matched 2 seeds and returned a 29-node, 42-edge subgraph, with every fact labelled as observed (31) or inferred (42). Seeds can also be a route, an API, a module, a native export or a node ID. Matching is exact or literal substring — no regular expressions.

In practice you rarely build this JSON by hand. With REA connected to your agent (see REA with Claude Code), ask:

prompt
Use REA to trace what happens when I click Export in /absolute/path/to/app.asar. Show the code at each step.

Real example: Notion’s clipboard bridge

REA’s Notion case study (Notion Desktop 7.6.1) shows the pattern on a production app. Static analysis of the tab preload pointed to two places in the minified bundle: the exposed API (__electronApi) and an ipcRenderer.invoke call. Following them, simplified from the packaged code:

preload.js → main/index.js (simplified)
// preload: the page calls window.__electronApi.clipboard.write(data)
const clipboardApi = { write: invokerInMain("notion:clipboard:write") };
function invokerInMain(channel) {
  return (...args) => ipcRenderer.invoke(channel, ...args);
}

// main process: a helper registers ipcMain.handle for the same channel
handleRequestFromRenderer.addListener("notion:clipboard:write", (event, data) =>
  isNotionWebContents(event.sender) ? clipboard.write(data) : undefined
);

The channel string joins both sides, and the main process checks the sender before writing — the kind of detail REA’s sender_validation_observations summary is there to point you to.

4. Observe the running app

Static analysis shows what can happen. To see what a page actually loads, start the app with Chromium’s remote-debugging switch (if the app honors it) and let REA attach passively over loopback:

shell
/Applications/AppName.app/Contents/MacOS/AppName --remote-debugging-port=9223

npx -y rea-agents@latest list-electron-targets http://127.0.0.1:9223 --json
npx -y rea-agents@latest inspect-electron-page http://127.0.0.1:9223 TARGET_ID --observation-ms 100 --json

Inspection doesn’t evaluate JavaScript, click, invoke IPC or close anything. Add --include-script-sources to retrieve the scripts the page loaded. REA only accepts literal loopback endpoints, and anything the endpoint exposes — including local file paths — becomes visible to your agent.

Limits & next steps

  • Computed channel names and ambiguous handlers can stay unresolved; REA reports them as unknowns rather than guessing.
  • A missing relation is never treated as proof that code is unused — static paths are inferences until a runtime capture confirms them.
  • Native add-ons (.node files) are handed off: the trace names the exact binary and export so you can continue with native analysis.
  • Check the app’s license and terms, and only analyze software you own or are authorized to examine.

See what others found in reverse-engineered Electron apps, or submit your own teardown.

Sources

Found something outdated? REA ships fast. Tell us at [email protected].

Keep going