Reverse Engineer an Electron App with REA: ASAR, Preload, IPC
On this page
Electron apps ship their logic as JavaScript inside an app.asar archive, split across a main process, one or more preload scripts and renderer pages. Reading one minified bundle tells you little; the interesting questions (“what happens when I click Export?”) cross all three layers. REA, the open-source project by morluto, reads the archive statically, builds an application graph of entrypoints, modules, preload bridges and IPC channels, and keeps unresolved parts marked as unknown instead of guessing.
Who this is for: developers who want to understand how an Electron app is wired, for debugging, auditing, interoperability or learning. You should be comfortable with a terminal and basic Electron concepts.
What you will learn:
- Inventory and extract an
app.asarwithout running the app - Map modules, imports and source maps from the static graph
- Find
contextBridgeAPIs, BrowserWindowwebPreferencesand preload paths - Follow an IPC channel from the renderer to its main-process handler
- Spot native
.nodeadd-ons and hand them to a native analyzer
The worked target is Joplin, the open-source (AGPL-3.0-or-later) note-taking app at github.com/laurent22/joplin. Any open-source Electron app, or one you built yourself, works the same way.
Prerequisites
-
Node.js and npm. REA is distributed as the
rea-agentsnpm package. -
REA 6.4.0 or newer. Either run commands one-off with
npx -y rea-agents@latest <command>, or install the CLI:npm install --global rea-agentsTo use REA from Claude Code, Cursor or Codex, run
npx rea-agents setupand restart the agent. See /en/getting-started-rea-mcp/. -
No Electron runtime is needed for static analysis. REA parses JavaScript as inert text; it never executes the target code.
-
Optional: Ghidra or Hopper for native add-ons (installation docs),
jqfor slicing large results, and Wakaru on Linux x64 if you want to unbundle minified scripts (covered below).
All commands below use the rea binary; prefix them with npx -y rea-agents@latest instead if you did not install globally.
Step 1: Warm up on the REA notes example
Before touching a real app, run the analysis on the six-file example published with the REA JavaScript guide. It is small enough that you can read every file and check what REA reports. In that example, renderer.js calls window.notes.exportCsv(), preload.js exposes it through contextBridge and invokes notes:export, and main.js handles that channel with ipcMain.handle.
rea analyze-javascript-application /absolute/path/to/notes-example --json
Why: it calibrates your expectations. The guide records this excerpt of normalized_result.summary.ipc, produced by REA 4.1.0 on that example:
{
"literal_channels": 1,
"renderer_transmissions": 1,
"main_handlers": 1,
"paired_renderer_transmissions": 1,
"ambiguous_renderer_transmissions": 0
}
One literal channel, one renderer call, one handler, one pairing. A real app will produce much larger numbers and some dynamic channels, but the fields mean the same thing.
Step 2: Locate the ASAR
Packaged Electron apps usually keep their code in resources/app.asar. For Joplin:
- macOS:
/Applications/Joplin.app/Contents/Resources/app.asar - Linux AppImage: run the AppImage with
--appimage-extract, then look undersquashfs-root/resources/ - Windows: the install directory’s
resources\folder
Next to it you may find app.asar.unpacked/. Electron packagers put files there that must exist on the real filesystem, most often native .node binaries. Keep both together; REA checks the unpacked companions too.
Copy the archive (and the .unpacked folder) into a working directory so your analysis never depends on the installed copy changing under you.
Step 3: Inventory and extract the archive
set -o pipefail
rea inspect-artifact /absolute/path/to/work/app.asar --json | jq . > inspection.json
Why: inspect-artifact returns the content-addressed inventory (paths, sizes, SHA-256 digests, ASAR containment) plus suggested next probes. pipefail keeps REA’s exit status if jq follows it.
If you want the files on disk to read alongside REA’s results:
rea extract-artifact /absolute/path/to/work/app.asar --json
The result tells you where the files were written.
ASAR integrity. Electron archives carry declared SHA-256 hashes for their entries. REA fails closed on a mismatch by default. Packaging tools commonly sign or strip .asar.unpacked native binaries after writing the header, so their hashes no longer match even in a perfectly normal release. When that happens, rerun with:
rea extract-artifact /absolute/path/to/work/app.asar \
--integrity-policy record-and-continue --json
The contradiction is then recorded with declared and observed hashes rather than silently ignored.
Step 4: Build the application graph
rea analyze-javascript-application /absolute/path/to/work/app.asar \
--integrity-policy record-and-continue --json > app-evidence.json
rea analyze /path/app.asar routes to the same static provider when you pass neither --provider nor --snapshot.
Why save to a file: complete Evidence for a real app is large. The REA CLI docs mention a 334 MB Evidence file for Obsidian whose summary view is about 10 KB. Project a summary from the saved file instead of reading it whole:
jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' \
app-evidence.json > app-view.json
rea inspect-analysis-view app-view.json
The summary gives artifact identity, statistics, Electron surface counts, coverage and limitations. Read the limitations first: they tell you which scopes are partial and why.
Progress lines (rea_progress) go to stderr while inventory, parsing and graph construction run, so stdout stays a clean JSON document.
If the analysis is slow or runs out of memory, the dedicated command accepts --max-heap-mb and --analysis-timeout-ms (defaults: 1024 MiB V8 old-space and 300000 ms per source). A source that exhausts its budget becomes an explicit unknown; the rest of the app is still analyzed.
Step 5: Read modules and imports
Page through modules with the same view mechanism:
jq -c '{source: {kind: "inline", evidence: .},
view: {kind: "page", collection: "modules", offset: 0, limit: 32}}' \
app-evidence.json > modules-view.json
rea inspect-analysis-view modules-view.json
Follow coverage.next_offset for the next page. Each module carries its path, digest and source locations.
What REA recovers here, all from static syntax:
package.jsonmetadata and the declaredmainentry- static
import, dynamicimport(), CommonJSrequire, workers and service workers - Webpack and Rspack chunk registrations and module factories, when they are AST literals
- routes, network endpoints, storage and vendor markers
Narrow the scope when you already know where to look. If your question is about the main process and preload only, select exact artifact-relative paths. Note that --source-path is on repository main and is not in the npm 6.4.0 release, which rejects the flag; it needs a source build or a later release (see released package and main):
rea analyze /absolute/path/to/work/app.asar \
--source-path package.json --source-path main.js --json
REA still inventories and verifies the whole archive, but parses only the selected files. Imports do not expand the selection, and unselected files are reported as outside-analysis-scope, so coverage is honestly partial. The paths above are placeholders: take the real entry path from package.json in your inventory.
Step 6: Use source maps when the app ships them
Some apps include .map files or inline sourceMappingURL comments. REA parses local source-map declarations and records the original source names and content digests as maps_to edges in the graph. It does not store raw sourcesContent text in the graph.
Why it matters: original paths like src/services/sync/Synchronizer.ts are far easier to reason about than chunk-4f2a.js. Look for source-map nodes in the module pages; if none appear, the app simply did not ship maps.
No maps, heavily bundled code? REA can derive readable modules from a bundle with a caller-supplied Wakaru binary. This adapter is supported on Linux x64 only:
export REA_WAKARU_COMMAND="$PWD/wakaru"
rea recover-javascript-sources /tmp/work/main.js /tmp/recovered-main --json
rea analyze-javascript-application /tmp/recovered-main/modules --json
The output directory must not exist yet. Recovered modules are derived evidence: original variable names and runtime equivalence remain unknown. Details are in javascript-recovery.md.
Step 7: Find the preload bridge
The preload script is the contract between the sandboxed page and Node.js. REA reports:
- explicit
BrowserWindowoptions andwebPreferences, including statically resolvablepreloadpaths contextBridge.exposeInMainWorldandexposeInIsolatedWorldAPI keys and literal member paths- sender, frame, URL and origin validation candidates
Two caveats keep you honest:
- REA reports only
webPreferencesvalues that are explicitly present. It does not substitute Electron’s version-dependent defaults for omitted options. IfcontextIsolationis absent from the source, the answer is “not set in code”, not “true”. - A visible origin check is a candidate. REA does not claim it enforces a complete policy.
Real apps often build the bridge object dynamically. The REA Notion showcase is a useful reference for what that looks like: its preload exposed __electronApi with api_status: "dynamic", no literal members, and an invoke whose channel expression was a variable (channel: null, resolution: "dynamic"). The analyst then read the packaged wrapper code to find the literal channel. Treat that case as a lesson in reading dynamic bridges, nothing more.
Step 8: Follow an IPC channel
IPC pairing in REA is an inference with strict rules:
- a renderer
invokepairs only with oneipcMain.handle/handleOnceon the same literal channel - a renderer
sendpairs only with oneipcMain.on/once/addListeneron that channel - dynamic channels are never paired; multiple compatible handlers are reported as ambiguous
To trace a feature, build a trace input from the saved Evidence. This pattern comes from the REA workflow docs; replace the seed with a channel you found in the summary:
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: "channel", value: "your:channel-name" }, direction: "both"
}));'
rea trace-application-feature ./trace-input.json --json
Seed kinds are node-id, route, string, api, channel, module and native-export. Matching is literal: string seeds default to substring matching, the others to exact matching. No regular expressions.
The result contains the matching seeds, the complete reachable subgraph, terminal paths and any native handoffs. A matched channel still does not prove that the handler is registered at runtime or reached in a given session; it proves the static link.
An api seed is often the best start: a member like notes.exportCsv in the example leads through the preload to the handler in one trace.
Step 9: Native add-ons
When JavaScript calls require("./build/Release/something.node") or loads a native dependency, REA records a native add-on node and the member names requested by JavaScript syntax. It does not parse or execute the add-on, and those names are not verified exports.
To go further:
-
Trace with
{ "kind": "native-export", "value": "<name>" }to see which features reach that binding. If no matching native Evidence exists, the handoff reportsrequires-provider-analysisand names the exact add-on digest. -
Analyze the
.nodefile fromapp.asar.unpacked/with a native provider:rea analyze /absolute/path/to/work/app.asar.unpacked/path/to/addon.node --provider ghidra --json rea search /absolute/path/to/work/app.asar.unpacked/path/to/addon.node "Init" --provider ghidra --json
If the .unpacked folder was not supplied, the entry stays unavailable rather than being treated as absent. The Ghidra workflow is covered in /en/rea-ghidra-native-binary/.
Step 10 (optional): Check the static picture at runtime
Static analysis says what the code can do. To see what a page actually loaded, REA can attach passively to an Electron CDP endpoint on loopback. Only do this on apps and machines you are authorized to examine (see REA’s DISCLAIMER.md).
Start the app with a remote debugging port, for example --remote-debugging-port=9223. This is Electron’s own Chromium switch (Electron command-line switches), not a REA option; REA’s docs only require a literal-loopback CDP endpoint, and some apps disable or ignore the switch. Then:
rea list-electron-targets http://127.0.0.1:9223 --json
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID --observation-ms 100 --json
Inspection does not evaluate JavaScript, click, invoke IPC or close the app. For active scenarios that launch the app and record IPC channel and argument-shape metadata (never payload values), see rea capture-electron-scenario in electron-observation.md. Note that a scenario launches the app with your normal user privileges; it is not a sandbox.
Troubleshooting
Integrity failure on an .asar.unpacked file. Expected for many signed releases. Rerun with --integrity-policy record-and-continue; coverage becomes partial and the contradiction is listed.
“Unknown path” when using --source-path (main-only, not in 6.4.0). Paths must match inventoried files exactly, including nested ASAR prefixes. Globs and aliases are rejected. Copy the path from the inventory.
The IPC summary shows zero paired calls. Most likely the channels are built at runtime (a variable, a template string, or a wrapper function). Look at the dynamic channel locations in the graph and read that wrapper, as in the Notion case.
summary detail refused over MCP with capability_unavailable. The connected server does not retain Evidence. Use the CLI file-based view shown in Step 4, or update your REA registration with npx rea-agents@latest setup.
Output too big for the agent. Use summary and page views, --source-path selection (main only), or --format json --filter-output evidence_id,normalized_result.statistics to select fields.
Wakaru recovery refuses to start. Check that REA_WAKARU_COMMAND is an absolute path to the platform binary, not the Node.js shim in node_modules/.bin, and that you are on Linux x64.
Next steps
- Install and connect REA to your agent: /en/getting-started-rea-mcp/
- Continue into a native add-on: /en/rea-ghidra-native-binary/
- Compare two versions of the same app: /en/rea-evidence-snapshots-compare/
- Trace API calls in a web front end: /en/trace-website-api-calls/
- Reference docs: JavaScript artifact reconstruction, application workflows