src/skills/desktop-security-electron/SKILL.md
Electron fuses, ASAR integrity, sandbox hardening, CSP, permission handling, navigation restrictions
npx skillsauth add agents-inc/skills desktop-security-electronInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Electron fuses are compile-time security flags flipped via
@electron/fusesbefore code signing. DisableRunAsNode,EnableNodeOptionsEnvironmentVariable,EnableNodeCliInspectArguments, andGrantFileProtocolExtraPrivileges. EnableEnableCookieEncryption,EnableEmbeddedAsarIntegrityValidation, andOnlyLoadAppFromAsar. Rely on secure defaults:contextIsolation: true(Electron 12+),sandbox: true(Electron 20+),nodeIntegration: false(Electron 5+). Set a restrictive Content Security Policy. UsesetPermissionRequestHandlerto deny all permissions except an explicit allowlist. Block navigation and new-window creation.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST flip fuses BEFORE code signing -- the OS enforces fuse state after signing, so flipping after signing invalidates the signature)
(You MUST disable RunAsNode, EnableNodeOptionsEnvironmentVariable, EnableNodeCliInspectArguments, and GrantFileProtocolExtraPrivileges fuses in production builds -- these are the most commonly exploited attack vectors)
(You MUST enable both EnableEmbeddedAsarIntegrityValidation AND OnlyLoadAppFromAsar together -- enabling integrity validation alone still allows bypassing via the app code search path)
(You MUST NOT override security defaults (contextIsolation: true, sandbox: true, nodeIntegration: false) -- each one unlocks a critical attack surface)
(You MUST restrict permissions via session.setPermissionRequestHandler() -- Electron grants most permissions by default, including camera, microphone, and geolocation)
</critical_requirements>
Auto-detection: Electron fuses, @electron/fuses, FuseV1Options, FuseVersion, flipFuses, RunAsNode, EnableCookieEncryption, EnableEmbeddedAsarIntegrityValidation, OnlyLoadAppFromAsar, GrantFileProtocolExtraPrivileges, ASAR integrity, Electron security, contextIsolation, sandbox, nodeIntegration, webPreferences security, Content-Security-Policy, setPermissionRequestHandler, setPermissionCheckHandler, will-navigate, setWindowOpenHandler, shell.openExternal validation, deep link validation, registerSchemesAsPrivileged, ELECTRON_RUN_AS_NODE, NODE_OPTIONS
When to use:
shell.openExternal() callsWhen NOT to use:
Key patterns covered:
@electron/fuses (all 9 fuses explained)shell.openExternal() URL validationfile:// protocol replacement with custom schemesElectron security follows a defense-in-depth strategy: multiple independent layers each reduce attack surface, so a breach in one layer does not compromise the entire app.
The layers from outermost to innermost:
Key principle: Never rely on a single security mechanism. Each layer handles a different class of attack. Fuses stop living-off-the-land attacks. Sandboxing stops renderer-to-OS escalation. CSP stops code injection. Permission handlers stop unauthorized API access.
When to use this skill:
When NOT to use:
Fuses are compile-time feature toggles embedded in the Electron binary. Once flipped and code-signed, the OS prevents reversal. Use the @electron/fuses package to flip them during your build step.
// build-scripts/flip-fuses.js
const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses");
await flipFuses(
// Path to your packaged Electron binary
require("electron"),
{
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
[FuseV1Options.GrantFileProtocolExtraPrivileges]: false,
},
);
Why these settings: Disabling RunAsNode prevents ELECTRON_RUN_AS_NODE living-off-the-land attacks. Disabling GrantFileProtocolExtraPrivileges removes file:// fetch/frame escalation. Enabling cookie encryption and ASAR integrity adds tamper protection. See examples/core.md for the full fuse reference table and integration with Electron Forge and electron-builder.
ASAR integrity ensures the packaged app.asar has not been tampered with. Requires two fuses enabled together.
| Fuse | Purpose |
| --------------------------------------- | ------------------------------------------------------- |
| EnableEmbeddedAsarIntegrityValidation | Validates the ASAR header hash at runtime |
| OnlyLoadAppFromAsar | Prevents loading app code from outside the ASAR archive |
Platform support: macOS (Electron 16+), Windows (Electron 30+).
Key point: Electron Forge (7.4.0+) and @electron/packager (18.3.1+) automatically generate the integrity metadata when ASAR is enabled. Electron Builder supports it via the electronFuses config or afterPack hook. If only EnableEmbeddedAsarIntegrityValidation is enabled without OnlyLoadAppFromAsar, an attacker can place a plain app folder alongside the ASAR and bypass validation entirely.
See examples/core.md for platform-specific configuration details.
Modern Electron (v20+) defaults to the secure configuration. Never override these defaults.
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "preload.js"),
// All of these are DEFAULTS -- do NOT set them explicitly unless
// you need to verify them in a security audit
// contextIsolation: true -- Electron 12+
// sandbox: true -- Electron 20+
// nodeIntegration: false -- Electron 5+
// webSecurity: true -- always
},
});
Why good: Relying on defaults means future Electron upgrades automatically apply new secure defaults. Explicitly setting values can mask future changes.
// BAD -- overriding secure defaults
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: false, // exposes preload scope to renderer
nodeIntegration: true, // gives renderer full Node.js access
sandbox: false, // disables OS-level process isolation
},
});
Why bad: Each override removes a security layer. contextIsolation: false lets renderer code access preload globals. nodeIntegration: true gives the renderer fs, child_process, and every Node.js API. sandbox: false removes OS-level process isolation.
See examples/core.md for the development-only audit pattern that catches accidental overrides.
Set CSP to restrict what the renderer can load and execute. Use the <meta> tag for static apps or session headers for dynamic CSP.
<!-- index.html -- static CSP -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
"
/>
Key point: 'self' restricts loading to the app's own origin. Never add 'unsafe-eval' in production -- it enables eval() and template injection attacks. See examples/csp-permissions.md for session header CSP and development-specific overrides.
By default, Electron grants most web API permissions (camera, microphone, geolocation, notifications). Use an explicit allowlist to deny everything else.
const ALLOWED_PERMISSIONS = new Set([
"clipboard-read",
"clipboard-sanitized-write",
]);
session.defaultSession.setPermissionRequestHandler(
(_webContents, permission, callback) => {
callback(ALLOWED_PERMISSIONS.has(permission));
},
);
session.defaultSession.setPermissionCheckHandler((_webContents, permission) => {
return ALLOWED_PERMISSIONS.has(permission);
});
Why both handlers: setPermissionRequestHandler handles async permission requests (prompted). setPermissionCheckHandler handles synchronous permission queries (silent checks). Without both, some permissions can slip through. See examples/csp-permissions.md for extended examples.
Prevent the renderer from navigating to unexpected URLs or opening new windows.
// Block navigation away from the app
mainWindow.webContents.on("will-navigate", (event, url) => {
const appUrl = mainWindow.webContents.getURL();
if (url !== appUrl) {
event.preventDefault();
}
});
// Block new window creation -- open external links in default browser
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const parsed = new URL(url);
if (parsed.protocol === "https:") {
shell.openExternal(url);
}
return { action: "deny" };
});
Why critical: Without these guards, a compromised renderer could navigate to a phishing page styled to look like your app, or spawn pop-up windows. setWindowOpenHandler replaces the deprecated new-window event. See examples/navigation-protocols.md for complete patterns.
Custom protocols (myapp://) replace file:// for serving local content and handling deep links. Always validate incoming URLs.
protocol.registerSchemesAsPrivileged([
{
scheme: "myapp",
privileges: { standard: true, secure: true, supportFetchAPI: true },
},
]);
Key point: secure: true makes the scheme behave like https: for CSP and mixed-content checks. Deep link URLs must be parsed and validated before acting on them -- an attacker can craft arbitrary myapp:// URLs to trigger unintended actions. Disable GrantFileProtocolExtraPrivileges fuse and serve content via your custom protocol instead of file://.
See examples/navigation-protocols.md for deep link validation, safe shell.openExternal(), and custom protocol handler patterns.
<decision_framework>
For each fuse, decide based on your app's needs:
RunAsNode
+-- Does your app need ELECTRON_RUN_AS_NODE?
| +-- YES (rare -- testing tools only) --> Leave enabled
| +-- NO (most apps) --> Disable (prevents living-off-the-land attacks)
EnableCookieEncryption
+-- Does your app store cookies?
| +-- YES --> Enable (one-way transition -- cannot decrypt after enabling)
| +-- NO --> Enable anyway (no downside, protects future use)
EnableNodeOptionsEnvironmentVariable
+-- Does your production app need NODE_OPTIONS?
| +-- YES (extremely rare) --> Leave enabled
| +-- NO --> Disable (prevents env-var code injection)
EnableNodeCliInspectArguments
+-- Does your production app need --inspect?
| +-- YES (never in production) --> Leave enabled only for dev builds
| +-- NO --> Disable (prevents debugger attachment)
EnableEmbeddedAsarIntegrityValidation
+-- Is your app packaged as ASAR?
| +-- YES --> Enable (ALWAYS pair with OnlyLoadAppFromAsar)
| +-- NO --> Not applicable
OnlyLoadAppFromAsar
+-- Is ASAR integrity validation enabled?
| +-- YES --> Enable (prevents bypass via unpacked app folder)
| +-- NO --> Enable anyway (prevents loading tampered code)
GrantFileProtocolExtraPrivileges
+-- Does your app serve content via file:// protocol?
| +-- YES --> Migrate to custom protocol, then disable
| +-- NO --> Disable (removes file:// fetch and frame escalation)
What content does your renderer load?
+-- Only local files (bundled with app)?
| +-- Use: default-src 'self'; script-src 'self'
+-- Local files + CDN assets?
| +-- Use: default-src 'self'; script-src 'self' https://cdn.example.com
+-- Remote content (web app in a window)?
| +-- Use: restrictive CSP + sandbox: true + disable nodeIntegration
+-- Dev server with HMR?
+-- Use: relaxed CSP in dev only, strict in production
</decision_framework>
Detailed resources:
<red_flags>
Critical Security Issues:
contextIsolation (contextIsolation: false) -- exposes preload globals to renderer, enabling full preload scope access from untrusted codenodeIntegration: true -- gives renderer full Node.js access (fs, child_process, net), equivalent to giving a web page OS-level controlEnableEmbeddedAsarIntegrityValidation WITHOUT OnlyLoadAppFromAsar -- attacker can bypass validation by placing an app folder next to the ASARRunAsNode enabled in production -- allows ELECTRON_RUN_AS_NODE=1 /path/to/your/app malicious-script.js to execute arbitrary codeGrantFileProtocolExtraPrivileges enabled when not using file:// -- grants file:// pages fetch access and universal frame access unnecessarilyshell.openExternal() -- can execute arbitrary commands via file:// or custom protocol URLsMedium Priority Issues:
Content-Security-Policy in renderer HTML -- XSS attacks can inject and execute arbitrary scriptssetPermissionRequestHandler -- Electron grants camera, microphone, geolocation by defaultwill-navigate unhandled) -- renderer can navigate to phishing pages'unsafe-eval' in CSP -- enables eval(), new Function(), and template injectionwebSecurity in production (webSecurity: false) -- disables same-origin policy entirelyallowRunningInsecureContent: true -- allows HTTP content on HTTPS pagesGotchas & Edge Cases:
setPermissionCheckHandler and setPermissionRequestHandler serve different purposes -- you need both for complete coverage (sync checks vs async requests)setWindowOpenHandler replaces the deprecated new-window event -- using the old event does nothing in recent Electron versions'unsafe-inline' for bundler-injected scripts -- scope this to dev builds only, never ship itstrictlyRequireAllFuses: true in @electron/fuses will fail if a new fuse is added in a future Electron version and you haven't configured it -- useful for staying current but breaks builds on Electron upgrades until you update configresetAdHocDarwinSignature must be true for arm64 macOS builds when not immediately code-signing afterward -- without it, the app fails to launch on Apple Silicon</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST flip fuses BEFORE code signing -- the OS enforces fuse state after signing, so flipping after signing invalidates the signature)
(You MUST disable RunAsNode, EnableNodeOptionsEnvironmentVariable, EnableNodeCliInspectArguments, and GrantFileProtocolExtraPrivileges fuses in production builds -- these are the most commonly exploited attack vectors)
(You MUST enable both EnableEmbeddedAsarIntegrityValidation AND OnlyLoadAppFromAsar together -- enabling integrity validation alone still allows bypassing via the app code search path)
(You MUST NOT override security defaults (contextIsolation: true, sandbox: true, nodeIntegration: false) -- each one unlocks a critical attack surface)
(You MUST restrict permissions via session.setPermissionRequestHandler() -- Electron grants most permissions by default, including camera, microphone, and geolocation)
Failure to follow these rules will create exploitable security vulnerabilities in your Electron application.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events