Protecting an Electron app's JavaScript: what actually works
If you sell or ship an Electron app, your JavaScript is on the customer's computer. That is not a failure of Electron. It is how the runtime works. The useful question is not "how do I hide the code completely" — you can
If you sell or ship an Electron app, your JavaScript is on the customer's computer. That is not a failure of Electron. It is how the runtime works. The useful question is not "how do I hide the code completely" — you cannot — but "what actually raises the cost of copying or patching it, and what only looks like protection?"
This guide is for people protecting their own commercial apps.
asar is a packing format, not a lock
The most common misconception is that putting your files in app.asar encrypts them. It does not. An asar archive is a concatenation of files with an index at the front, similar in spirit to a tarball. Anyone can unpack it in one command:
npx @electron/asar extract /path/to/YourApp.app/Contents/Resources/app.asar ./unpacked
# Windows: look under resources/app.asar inside the install directory
After that, your main.js, renderer bundles and any leftover src/ files are ordinary files on disk. If you have never unpacked your own release, do it now. You will often find source maps, TypeScript sources, .env samples or a readable license.js.
asar still has a purpose: it stops casual browsing of the install folder and lets Electron read files efficiently. Treat it as packaging, the way you treat a zip file.
Theater that does not hold up
A few other steps get advertised as "protection" and do not do what people hope.
Renaming app.asar or shipping app.asar.unpacked only. Unpacked directories are even easier to read. A renamed archive is still an asar.
Disabling DevTools and calling it done. You should disable DevTools in production and set Electron fuses so users cannot re-enable inspection with command-line flags. That blocks casual poking. It does not stop someone from unpacking the asar, editing a file and repacking it.
Fuses alone. Fuses such as RunAsNode, EnableNodeCliInspectArguments and EnableNodeOptionsEnvironmentVariable are worth turning off in production. They close real holes. They do not encrypt your source.
Minification of the whole app. A formatter restores layout in seconds. Minify anyway — it removes comments and shrinks the download — but do not count it as protection.
Start by shipping less
The cheapest win is not putting files in the asar at all.
- Bundle the main process and each renderer with esbuild, webpack or Vite. Ship a handful of files, not a
src/tree. - Set
filesinpackage.jsonor an explicit electron-builder allowlist so tests, fixtures and docs stay out. - Generate source maps for your crash reporter, upload them privately, and keep
*.mapout of the release. - Move anything that does not need to run offline — billing, license issuance, account status — to your server.
# electron-builder.yml (excerpt)
files:
- dist/**/*
- package.json
asar: true
asarUnpack:
- "**/*.node"
extraMetadata:
main: dist/main.js
Then confirm the result:
npx electron-builder --dir
npx @electron/asar extract dist/win-unpacked/resources/app.asar /tmp/asar-audit
find /tmp/asar-audit \( -name "*.map" -o -name "*.ts" -o -name ".env*" \)
If that audit prints anything you would not post on a public gist, fix the pack list before you think about obfuscation.
Main process and renderer are different problems
The main process is Node.js. It can read the filesystem, spawn processes and hold license state. The renderer is a browser. If you enable nodeIntegration or disable contextIsolation, a compromised or inspected renderer can reach Node APIs. For a production app:
- Keep
contextIsolation: trueandnodeIntegration: false. - Expose a narrow API through
contextBridge. - Put license checks and privileged work in the main process or a utility process, not in renderer code a user can edit with DevTools overrides.
// preload.js — keep this thin
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("appApi", {
getEntitlements: () => ipcRenderer.invoke("ents:get"),
});
The renderer then asks for entitlements instead of deciding them. The decision still lives on the user's machine if the app is offline-first, but it is no longer a one-line edit in a renderer bundle.
Harden the code that has to stay client-side
After you have shipped less and locked the Chromium settings, some logic still has to run locally: a license verifier that works offline, a proprietary transform, a rules engine, a file-format parser. That is the part worth protecting.
Plain-JavaScript obfuscation (identifier renaming, string encoding, control-flow flattening) makes casual search-and-patch harder. Public undo scripts exist for many of those transforms, so treat it as a first layer.
VM-based protection goes further. It compiles selected functions into a custom bytecode and ships a small interpreter that runs it. The original if (license.valid) shape is no longer sitting in the file. An analyst has to understand the interpreter first. That raises cost and time. It is not unbreakable, and interpreted code runs slower than native JavaScript, so you apply it to a short list of functions, not to your UI or to node_modules.
A practical split looks like this:
- UI, window management, logging: minify only
- General business code: standard obfuscation
- License verification, core algorithm: VM protection, with an integrity check inside the protected code
- Signing keys, billing secrets: never in the app
If you want to try VM protection on a real module, VEILVM is a browser-based JSVMP workspace. You can run a file through a high-performance or dynamic VM engine without installing a toolchain. The point is selective protection of the modules you just identified, not wrapping the entire renderer.
Put those steps in CI so a release build cannot forget them. A plan with a CI API is useful here; a manual "obfuscate then paste" step gets skipped under deadline pressure.
Integrity and updates
electron-builder can embed asar integrity checks so a swapped archive fails to load on some platforms. Combine that with signed installers (Windows Authenticode, macOS notarization). Integrity does not hide code, but it stops the "edit one file and zip it back up" path that most copiers try first.
Your updater should also refuse unsigned or mismatched payloads. Otherwise an attacker ships a patched asar as a "mod" or a fake update.
What this will not do
Anyone who controls the machine can attach a debugger, dump memory or hook Electron's JavaScript bindings. Client-side protection buys time and filters out casual copying. It does not replace server-side enforcement for purchases, seats or online features, and it will not make a secret key in the renderer private.
Do not put third-party API secrets, webhook signing keys or admin flags in the asar, protected or not.
A release checklist
- Unpack your own asar and look at every file.
- Remove source maps, sources and secrets from the pack list.
- Turn on context isolation, turn off
nodeIntegration, disable production DevTools and lock the relevant fuses. - Sign the installer and enable asar integrity where your builder supports it.
- Protect only the high-value modules, and automate that in the release pipeline.
- Keep a private, unprotected build for your own debugging.
Try it on a license module
VEILVM lets you try two VM engines on a real file in the browser with no sign-up: 3 free runs a day, up to 5 KB per file (resets at 00:00 UTC). A free account raises the input limit to 30 KB. Pro is $9/month or $79/year and adds a CI API so the protection step is part of electron-builder, not a manual afterthought. Standard requests are processed in memory and are not written to logs. Start at https://vm.ldvmp.com.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.