Rule Writing

CLAUDE.md for Electron Apps

Electron has two processes with different security models. AI treats it like a web app. Rules for main/renderer split, IPC, contextBridge, and security hardening.

7 min read·June 5, 2025

Electron + nodeIntegration: true = every XSS gives full system access

Main/renderer split, IPC, contextBridge, and security hardening

Why Electron Needs Security-First Rules

Electron apps run a Chromium browser with Node.js access — the most powerful and most dangerous combination in desktop software. If nodeIntegration is enabled in the renderer (the default in old tutorials), any XSS vulnerability gives the attacker full system access: file system, network, child processes, everything. AI generates Electron code from outdated tutorials that enable nodeIntegration, skip contextIsolation, and expose the entire Node API to the renderer.

The modern Electron security model separates main (Node.js, full system access) from renderer (browser, sandboxed). Communication happens through IPC. The renderer only accesses system capabilities through a controlled API exposed via contextBridge. AI must generate code that follows this model — not the insecure shortcuts from 2018 tutorials.

These rules target Electron 28+ with the recommended security configuration. Every rule exists to prevent a specific class of security vulnerability.

Rule 1: Main Process vs Renderer Process

The rule: 'The main process (main.js/main.ts) has full Node.js access — file system, child processes, native modules. The renderer process (loaded HTML/JS) runs in a sandboxed Chromium browser — no Node.js access. All communication between them uses IPC. Never enable nodeIntegration in the renderer. Never disable contextIsolation. Never disable the sandbox.'

For BrowserWindow config: 'webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true, preload: path.join(__dirname, "preload.js") }. These four settings are non-negotiable. nodeIntegration: false prevents Node access in the renderer. contextIsolation: true prevents prototype pollution. sandbox: true adds OS-level process isolation. preload: specifies the bridge script.'

AI generates BrowserWindow without webPreferences (insecure defaults in older Electron) or with nodeIntegration: true (every tutorial from 2018-2020 does this). One security config block prevents the entire class of Electron privilege escalation vulnerabilities.

  • Main process: Node.js, file system, native modules, system access
  • Renderer: sandboxed browser — no Node, no fs, no child_process
  • nodeIntegration: false — always, no exceptions
  • contextIsolation: true — prevents prototype pollution attacks
  • sandbox: true — OS-level process isolation
⚠️ 4 Non-Negotiable Settings

nodeIntegration: false, contextIsolation: true, sandbox: true, preload: set. AI generates BrowserWindow without webPreferences — or with nodeIntegration: true from 2018 tutorials. These four settings prevent the entire class of Electron privilege escalation.

Rule 2: IPC for All Cross-Process Communication

The rule: 'Use ipcMain.handle + ipcRenderer.invoke for request/response IPC (async, return values). Use ipcMain.on + event.reply for fire-and-forget messages. Use contextBridge.exposeInMainWorld in the preload script to create a typed API that the renderer can call. Never use remote module — it's deprecated and insecure. Never send entire objects — send only the data needed.'

For the preload script: 'preload.js bridges main and renderer: contextBridge.exposeInMainWorld("api", { readFile: (path) => ipcRenderer.invoke("read-file", path), saveFile: (path, content) => ipcRenderer.invoke("save-file", path, content) }). The renderer accesses window.api.readFile() — a controlled, typed interface. Never expose ipcRenderer directly — wrap every call in a named function.'

For main handlers: 'Register handlers with ipcMain.handle: ipcMain.handle("read-file", async (event, filePath) => { // Validate filePath — never trust renderer input return fs.readFile(filePath, "utf8"); }). Validate all inputs from the renderer — it's an untrusted source. Never pass paths directly to fs without validation.'

💡 Never Expose ipcRenderer

contextBridge.exposeInMainWorld('api', { readFile: (path) => ipcRenderer.invoke('read-file', path) }). Wrap every IPC call in a named function. The renderer sees window.api — a controlled, typed interface. Never expose raw ipcRenderer.

Rule 3: Security Hardening

The rule: 'Set a restrictive Content-Security-Policy: session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, "Content-Security-Policy": ["default-src 'self'; script-src 'self'"] } }) }). Disable navigation to external URLs: win.webContents.on("will-navigate", (event) => event.preventDefault()). Disable new window creation: app.on("web-contents-created", (event, contents) => { contents.setWindowOpenHandler(() => ({ action: "deny" })) }).'

For external content: 'Never load remote URLs in the main window — only local files (file:// or custom protocol). If you must display external content, use a <webview> tag with sandboxed permissions or open in the system browser: shell.openExternal(url). Validate all URLs before opening: only allow https:// with known domains.'

For updates: 'Use electron-updater for auto-updates. Sign the application with a code signing certificate. Verify update signatures before applying. Use HTTPS for update servers. Never download and execute unsigned code — this is the most common Electron supply chain attack vector.'

  • Content-Security-Policy: default-src 'self' — no inline scripts
  • Prevent navigation to external URLs — win.webContents will-navigate
  • Deny new window creation — setWindowOpenHandler returns deny
  • Never load remote URLs in main window — local files only
  • Code signing + signed updates — electron-updater with HTTPS
ℹ️ Renderer Is Untrusted

Validate ALL inputs from the renderer in ipcMain handlers — the renderer is an untrusted source (like a browser client). Never pass renderer-supplied paths directly to fs. An XSS in the renderer becomes a local file read without validation.

Rule 4: Application Architecture

The rule: 'Separate concerns across processes: main process handles file system, databases (SQLite via better-sqlite3), system tray, native menus, and auto-updates. Renderer handles UI (React, Vue, Svelte, or vanilla). Preload script exposes a typed API bridge. Use TypeScript in all three layers for type safety across the IPC boundary.'

For data persistence: 'Use better-sqlite3 or electron-store for local data. Database operations happen in the main process — the renderer requests data via IPC. For complex data, consider SQLite with Drizzle or Prisma (configured for SQLite). Never store sensitive data unencrypted — use safeStorage.encryptString() for credentials.'

For the build pipeline: 'Use electron-forge or electron-builder for packaging. Configure for all target platforms: Windows (NSIS/MSI), macOS (DMG), Linux (AppImage/deb/rpm). Use asar packaging for source code. Exclude development dependencies from the build. Set proper application metadata: name, version, author, description.'

Rule 5: Testing Electron Applications

The rule: 'Use Playwright or Spectron for end-to-end testing — they drive the actual Electron window. Test IPC channels: verify that renderer calls produce correct main process behavior. Unit test main process logic independently (it's just Node.js). Unit test renderer logic with standard web testing tools (Vitest, Testing Library). Test the preload API contract: verify exposed functions match expectations.'

For E2E testing: 'Playwright supports Electron natively: const electronApp = await electron.launch({ args: ["."] }); const page = await electronApp.firstWindow();. Interact with the UI: page.click, page.fill. Verify results: expect(await page.textContent(".status")).toBe("Saved"). Close: await electronApp.close().'

For CI: 'Electron tests need a display — use xvfb-run on Linux CI: xvfb-run --auto-servernum npx playwright test. On macOS CI, no display setup needed. On Windows CI, tests run natively. Package and test the built app, not just the dev version — build artifacts can differ.'

Complete Electron Rules Template

Consolidated rules for Electron desktop applications.

  • nodeIntegration: false, contextIsolation: true, sandbox: true — non-negotiable
  • contextBridge + preload for typed IPC API — never expose ipcRenderer directly
  • ipcMain.handle for request/response — validate all inputs from renderer
  • CSP: default-src 'self' — prevent navigation — deny new windows — local files only
  • Main: file system, SQLite, native menus — Renderer: UI only, sandboxed
  • safeStorage for credentials — electron-updater with code signing
  • electron-forge or electron-builder — asar packaging — all platform targets
  • Playwright for E2E — Vitest for unit — xvfb-run for Linux CI