Inspect Electron Fuses with REA: Read and Audit the Nine Security Switches
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
FuseV1switches controls, and which value is the hardened one - How to read the fuses from a
.appbundle, 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
Prerequisites
-
Node.js and npm. REA is distributed as the
rea-agentsnpm package. -
REA 6.5.0 or newer.
inspect-electron-fusesfirst 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 withnpx -y rea-agents@latest inspect-electron-fuses <path>, or install the CLI:npm install --global rea-agentsTo drive REA from Claude Code, Cursor or Codex, run
npx rea-agents setupand restart the agent. See /getting-started-rea-mcp/. -
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
.appbundle, or directlyContents/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-extractfirst and read the binary undersquashfs-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:
- A hardened fuse is not a proof of behavior. With
EnableNodeCliInspectArgumentsoff, 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. enforcedis read from one byte.embedded_asar_integrity_validation: enforcedmeans the fuse is on. It does not confirm that a correct hash is present in theInfo.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 theElectronAsarIntegrityhash (the ASAR analyzer shows both side by side).- Writing fuses can break code signing. If you harden your own macOS build with
@electron/fuses write, the modified framework will failcodesign --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 withcodesign --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
- Put fuses in context with the full app graph: Reverse Engineer an Electron App
- Extract the archive the fuses protect: Unpack an ASAR file
- Read fuses and ASAR integrity in the browser: ASAR analyzer
- Connect REA to your agent: /getting-started-rea-mcp/
- Reference: Electron fuses tutorial