rea-reverse

Inspect Electron Fuses with REA: Read and Audit the Nine Security Switches

Written for REA 6.7.0Published by rea-reverseUpdated 7 min read
On this page

An Electron app’s JavaScript cannot tell you whether the runtime around it is locked down. That decision is made once, at package time, and baked into the Electron binary as a short run of bytes called fuses. A fuse flips a feature on or off for good: whether ELECTRON_RUN_AS_NODE turns the executable into a plain Node.js process, whether a debugger may attach, whether the embedded app.asar is checked against a hash before it loads. Reading the app.asar tells you what the app does; reading the fuses tells you what the runtime allows the app’s environment to do to it. For a security review you need both.

This article is about the second half: pulling the nine fuses out of a packaged app with REA, the open-source project by morluto, and reading them the way an auditor would. REA 6.5.0 added inspect-electron-fuses, which reads the fuse bytes statically; it never launches the target. If you want the application-graph side first (ASAR, preload, IPC, native add-ons, and fuses in context), start with Reverse Engineer an Electron App and come back here for the detail.

Who this is for: developers hardening their own Electron builds, and reviewers auditing an app they are authorized to examine. You should be comfortable with a terminal.

What you will learn:

  • What each of the nine FuseV1 switches controls, and which value is the hardened one
  • How to read the fuses from a .app bundle, a framework binary, a Windows PE or a Linux ELF
  • How to read REA’s states, raw bytes and runtime implications
  • Why passing the wrong file gives an empty, misleading answer
  • How to cross-check the result with @electron/fuses, and what fuses still cannot prove
Read Electron’s Runtime SwitchesSilent walkthrough with English captions.

Prerequisites

  1. Node.js and npm. REA is distributed as the rea-agents npm package.

  2. REA 6.5.0 or newer. inspect-electron-fuses first shipped in 6.5.0. There is no 6.6.0 on npm; releases went from 6.5.0 to 6.7.0. Run it one-off with npx -y rea-agents@latest inspect-electron-fuses <path>, or install the CLI:

    npm install --global rea-agents

    To drive REA from Claude Code, Cursor or Codex, run npx rea-agents setup and restart the agent. See /getting-started-rea-mcp/.

  3. No Electron runtime is needed. REA scans the binary for a sentinel string and reads the bytes after it. It does not execute anything.

Commands below use the rea binary; prefix them with npx -y rea-agents@latest instead if you did not install globally.

The nine fuses

Electron stores fuses as a FuseV1 wire: a 32-byte sentinel, a version byte, a length byte, then one ASCII byte per fuse (0 disabled, 1 enabled, r removed). The order is fixed. Electron 44 ships nine fuses; older Electron 20 ships the first seven, so a short list is an old runtime, not a corrupt one.

# Fuse Enabled means Hardened
0 RunAsNode ELECTRON_RUN_AS_NODE=1 makes the executable run as a plain Node.js process: full Node access, none of Electron’s security settings. Disabled, child_process.fork throws and utilityProcess is the replacement. off
1 EnableCookieEncryption The on-disk cookie store is encrypted with OS keys (Keychain on macOS, DPAPI on Windows). One-way: once on, turning it off again corrupts the store. on
2 EnableNodeOptionsEnvironmentVariable NODE_OPTIONS and NODE_EXTRA_CA_CERTS are honored, so whoever starts the app can inject Node flags or trust extra CA certificates. off
3 EnableNodeCliInspectArguments --inspect, --inspect-brk and related flags are honored, so a debugger can attach to the main process. Disabled, SIGUSR1 does not start the inspector either. off
4 EnableEmbeddedAsarIntegrityValidation Electron checks app.asar against a hash stored at build time (Info.plist ElectronAsarIntegrity on macOS, a resource on Windows) and refuses a modified archive. on
5 OnlyLoadAppFromAsar The app is loaded only from app.asar; the app/ folder and default_app.asar are no longer searched. With integrity validation on, only the validated archive can run. on
6 LoadBrowserProcessSpecificV8Snapshot The main process uses its own V8 snapshot, browser_v8_context_snapshot.bin. A startup setting, not a hardening switch. —
7 GrantFileProtocolExtraPrivileges Pages loaded from file:// get privileges a normal browser withholds (reading other file:// URLs, service workers, universal access for file:// child frames). Apps that do not serve pages from file:// should turn it off. off
8 WasmTrapHandlers V8 uses signal handlers to trap out-of-bounds WebAssembly access (faster bounds checks). A performance setting, not a hardening switch. —

The four that matter most for an attacker with local access are RunAsNode, EnableNodeOptionsEnvironmentVariable, EnableNodeCliInspectArguments and the pair EnableEmbeddedAsarIntegrityValidation + OnlyLoadAppFromAsar. The first three each give code execution or a debugger inside the app’s context through an environment variable or a flag; the pair is what stops someone swapping your app.asar for their own.

Step 1: Point it at the right file

rea inspect-electron-fuses /absolute/path/to/NotesDemo.app --json > fuses.json

The argument is a local Electron .app bundle, the Electron Framework binary, a Windows PE executable or a Linux ELF executable. Given a macOS bundle, REA resolves it to the framework inside it, which is where the wire lives, not the launcher:

  • macOS: the .app bundle, or directly Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework
  • Windows: the app’s main .exe
  • Linux: the main binary (for an AppImage, run it with --appimage-extract first and read the binary under squashfs-root/)

The most common mistake is the wrong file. On macOS the fuses are in the framework, not in Contents/MacOS/<app>. Passing the launcher exits 0 with sentinel_count: 0 and no fuses. That is not “all fuses off”; REA is explicit that a missing sentinel means the state is unknown, not that every fuse is disabled. If you see zero sentinels, you almost certainly pointed at the launcher stub. Pass the bundle and let REA find the framework, or pass the framework binary yourself.

Step 2: Read the states

jq -c '.normalized_result.slices[0].sentinels[0].fuses[] | [.index, .name, .state]' fuses.json

On an unmodified @electron/packager build of a small demo app (the same NotesDemo from the Electron reverse-engineering walkthrough), REA resolved the bundle to Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework, found one sentinel, and reported:

[0,"RunAsNode","enabled"]
[1,"EnableCookieEncryption","disabled"]
[2,"EnableNodeOptionsEnvironmentVariable","enabled"]
[3,"EnableNodeCliInspectArguments","enabled"]
[4,"EnableEmbeddedAsarIntegrityValidation","disabled"]
[5,"OnlyLoadAppFromAsar","disabled"]
[6,"LoadBrowserProcessSpecificV8Snapshot","disabled"]
[7,"GrantFileProtocolExtraPrivileges","enabled"]
[8,"WasmTrapHandlers","enabled"]

That is the stock Electron 44 default (101100011 as raw bytes). Read against the hardened column above, a stock build is wide open: RunAsNode on, NODE_OPTIONS honored, the inspector reachable, and no ASAR integrity. For a shipping app every one of those should be the other way. Each fuse also carries a raw_byte field, 48 or 49 (ASCII 0 and 1), and a removed state reads as r (114) when the app author burned a fuse out entirely.

A universal macOS binary has one fuse wire per architecture slice, so slices holds more than one entry; check each. sentinel_offset on each sentinel is the byte offset where it was found, useful when you want to confirm two tools read the same spot.

Step 3: Read the runtime implications

States are the raw truth; REA also projects the four that change the attack surface into plain runtime_implications:

jq -c '.normalized_result.runtime_implications[] | {subject, outcome}' fuses.json

For the stock build above:

{"subject":"node_cli_inspect_arguments","outcome":"honored"}
{"subject":"electron_run_as_node","outcome":"honored"}
{"subject":"embedded_asar_integrity_validation","outcome":"not_enforced"}
{"subject":"only_load_app_from_asar","outcome":"not_restricted_to_app_asar"}

Harden the build (RunAsNode=off EnableNodeCliInspectArguments=off EnableEmbeddedAsarIntegrityValidation=on OnlyLoadAppFromAsar=on) and the same four flip to not_honored, not_honored, enforced and restricted_to_app_asar. These are the lines to put in a review: they name the behavior, not the byte.

Step 4: Cross-check with @electron/fuses

The fuse format is small and stable, but a second reader is cheap insurance. @electron/fuses is the official tool that writes these bytes, and it reads them too:

npx @electron/fuses read --app /absolute/path/to/NotesDemo.app

On version 2.1.3 it prints the same nine states in the same order as REA. REA’s wire parsing was verified against @electron/fuses 2.1.3 (its dist/constants.js, config.js and index.js), including the sentinel string and the FuseV1Options order, so agreement is expected; a disagreement means you fed the two tools different files (one the launcher, one the framework) or different architecture slices.

What fuses do not tell you

Fuses are a necessary check, not a complete one. Three limits are worth stating in any review:

  1. A hardened fuse is not a proof of behavior. With EnableNodeCliInspectArguments off, a build can still accept Chromium’s own --remote-debugging-port=<n> and expose a page to CDP: that switch is not gated by this fuse. REA’s own limitation says it plainly: fuse state “does not prove the application enables Inspector through other means.” Fuses close specific doors; they do not audit the whole house.
  2. enforced is read from one byte. embedded_asar_integrity_validation: enforced means the fuse is on. It does not confirm that a correct hash is present in the Info.plist (macOS) or the Windows resource, nor that it matches your archive. To check the integrity data, compare the archive’s header SHA-256 with the ElectronAsarIntegrity hash (the ASAR analyzer shows both side by side).
  3. Writing fuses can break code signing. If you harden your own macOS build with @electron/fuses write, the modified framework will fail codesign --verify (“code or signature have been modified”) and the app may be killed on launch (exit 137) on arm64. The CLI does not re-sign; re-sign ad hoc with codesign --force --deep --sign - NotesDemo.app. This is a packaging concern, not a REA one, but you will hit it the first time you test fuses this way.

Without installing anything

If you would rather not install the CLI, the browser-based ASAR analyzer reads the same fuse wire. Drop the Electron executable on it instead of the app.asar: the framework binary on macOS, the main .exe on Windows, the main binary on Linux. It shows each fuse as enabled, disabled or removed, what it controls, and whether it matches the hardened value, with one table per architecture slice for a universal binary. It was checked against @electron/fuses read 2.1.3 on binaries from the official Electron release zips; the fuse names and order follow FuseV1Options from @electron/fuses 2.1.3, the same source REA uses, so the two agree. Everything runs locally and nothing is uploaded.

To get the app.asar out of a packaged app in the first place, see Unpack an ASAR file.

Next steps