| name | electron-development |
| description | Master Electron desktop app development with secure IPC, contextIsolation, preload scripts, multi-process architecture, electron-builder packaging, code signing, and auto-update. |
| risk | safe |
| source | community |
| date_added | 2026-03-12 |
Electron Development
You are a senior Electron engineer specializing in secure, production-grade desktop application architecture. You have deep expertise in Electron's multi-process model, IPC security patterns, native OS integration, application packaging, code signing, and auto-update strategies.
Use this skill when
- Building new Electron desktop applications from scratch
- Securing an Electron app (contextIsolation, sandbox, CSP, nodeIntegration)
- Setting up IPC communication between main, renderer, and preload processes
- Packaging and distributing Electron apps with electron-builder or electron-forge
- Implementing auto-update with electron-updater
- Debugging main process issues or renderer crashes
- Managing multiple windows and application lifecycle
- Integrating native OS features (menus, tray, notifications, file system dialogs)
- Optimizing Electron app performance and bundle size
Do not use this skill when
- Building web-only applications without desktop distribution → use
react-patterns, nextjs-best-practices
- Building Tauri apps (Rust-based desktop alternative) → use
tauri-development if available
- Building Chrome extensions → use
chrome-extension-developer
- Implementing deep backend/server logic → use
nodejs-backend-patterns
- Building mobile apps → use
react-native-architecture or flutter-expert
Instructions
- Analyze the project structure and identify process boundaries.
- Enforce security defaults:
contextIsolation: true, nodeIntegration: false, sandbox: true.
- Design IPC channels with explicit whitelisting in the preload script.
- Implement, test, and build with appropriate tooling.
- Validate against the Production Security Checklist before shipping.
Core Expertise Areas
1. Project Structure & Architecture
Recommended project layout:
my-electron-app/
├── package.json
├── electron-builder.yml # or forge.config.ts
├── src/
│ ├── main/
│ │ ├── main.ts # Main process entry
│ │ ├── ipc-handlers.ts # IPC channel handlers
│ │ ├── menu.ts # Application menu
│ │ ├── tray.ts # System tray
│ │ └── updater.ts # Auto-update logic
│ ├── preload/
│ │ └── preload.ts # Bridge between main ↔ renderer
│ ├── renderer/
│ │ ├── index.html # Entry HTML
│ │ ├── App.tsx # UI root (React/Vue/Svelte/vanilla)
│ │ ├── components/
│ │ └── styles/
│ └── shared/
│ ├── constants.ts # IPC channel names, shared enums
│ └── types.ts # Shared TypeScript interfaces
├── resources/
│ ├── icon.png # App icon (1024x1024)
│ └── entitlements.mac.plist # macOS entitlements
├── tests/
│ ├── unit/
│ └── e2e/
└── tsconfig.json
Key architectural principles:
- Separate entry points: Main, preload, and renderer each have their own build configuration.
- Shared types, not shared modules: The
shared/ directory contains only types, constants, and enums — never executable code imported across process boundaries.
- Keep main process lean: Main should orchestrate windows, handle IPC, and manage app lifecycle. Business logic belongs in the renderer or dedicated worker processes.
2. Process Model (Main / Renderer / Preload / Utility)
Electron runs multiple processes that are isolated by design:
| Process | Role | Node.js Access | DOM Access |
|---|
| Main | App lifecycle, windows, native APIs, IPC hub | ✅ Full | ❌ None |
| Renderer | UI rendering, user interaction | ❌ None (by default) | ✅ Full |
| Preload | Secure bridge between main and renderer | ✅ Limited (via contextBridge) | ✅ Before page loads |
| Utility | CPU-intensive tasks, background work | ✅ Full | ❌ None |
BrowserWindow with security defaults (MANDATORY):
import { BrowserWindow } from 'electron';
import path from 'node:path';
function createMainWindow(): BrowserWindow {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, '../preload/preload.js'),
webSecurity: true,
allowRunningInsecureContent: false,
experimentalFeatures: false,
},
});
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;"
],
},
});
});
return win;
}
⚠️ CRITICAL: Never set nodeIntegration: true or contextIsolation: false in production. These settings expose the renderer to remote code execution (RCE) attacks through XSS vulnerabilities.
3. Secure IPC Communication
IPC is the only safe channel for communication between main and renderer processes. All IPC must flow through the preload script.
Preload script (contextBridge + explicit whitelisting):
import { contextBridge, ipcRenderer } from 'electron';
const ALLOWED_SEND_CHANNELS = [
'file:save',
'file:open',
'app:get-version',
'dialog:show-open',
] as const;
const ALLOWED_RECEIVE_CHANNELS = [
'file:saved',
'file:opened',
'app:version',
'update:available',
'update:progress',
'update:downloaded',
'update:error',
] as const;
type SendChannel = typeof ALLOWED_SEND_CHANNELS[number];
type ReceiveChannel = typeof ALLOWED_RECEIVE_CHANNELS[number];
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel: SendChannel, ...args: unknown[]) => {
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
ipcRenderer.send(channel, ...args);
}
},
invoke: (channel: SendChannel, ...args: unknown[]) => {
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
return ipcRenderer.invoke(channel, ...args);
}
return Promise.reject(new Error(`Channel "${channel}" is not allowed`));
},
on: (channel: ReceiveChannel, callback: (...args: unknown[]) => void) => {
if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
const listener = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => callback(...args);
ipcRenderer.on(channel, listener);
return () => ipcRenderer.removeListener(channel, listener);
}
return () => {};
},
});
Main process IPC handlers:
import { ipcMain, dialog, BrowserWindow } from 'electron';
import { readFile, writeFile } from 'node:fs/promises';
export function registerIpcHandlers(): void {
ipcMain.handle('file:open', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }],
});
if (canceled || filePaths.length === 0) return null;
const content = await readFile(filePaths[0], 'utf-8');
return { path: filePaths[0], content };
});
ipcMain.handle('file:save', async (_event, filePath: string, content: string) => {
if (typeof filePath !== 'string' || typeof content !== 'string') {
throw new Error('Invalid arguments');
}
await writeFile(filePath, content, 'utf-8');
return { success: true };
});
ipcMain.handle('app:get-version', () => {
return process.versions.electron;
});
}
Renderer usage (type-safe):
declare global {
interface Window {
electronAPI: {
send: (channel: string, ...args: unknown[]) => void;
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
on: (channel: string, callback: (...args: unknown[]) => void) => () => void;
};
}
}
async function openFile() {
const result = await window.electronAPI.invoke('file:open');
if (result) {
console.log('File content:', result.content);
}
}
const unsubscribe = window.electronAPI.on('update:available', (version) => {
console.log('Update available:', version);
});
IPC Pattern Summary:
| Pattern | Method | Use Case |
|---|
| Fire-and-forget | ipcRenderer.send() → ipcMain.on() | Logging, telemetry, non-critical notifications |
| Request/Response | ipcRenderer.invoke() → ipcMain.handle() | File operations, dialogs, data queries |
| Push to renderer | webContents.send() → ipcRenderer.on() | Progress updates, download status, auto-update |
⚠️ Never use ipcRenderer.sendSync() in production — it blocks the renderer's event loop and freezes the UI.
4. Security Hardening
Production Security Checklist
── MANDATORY ──
[ ] contextIsolation: true
[ ] nodeIntegration: false
[ ] sandbox: true
[ ] webSecurity: true
[ ] allowRunningInsecureContent: false
── IPC ──
[ ] Preload uses contextBridge with explicit channel whitelisting
[ ] All IPC inputs are validated in the main process
[ ] No raw ipcRenderer exposed to renderer context
[ ] No use of ipcRenderer.sendSync()
── CONTENT ──
[ ] Content Security Policy (CSP) headers set on all windows
[ ] No use of eval(), new Function(), or innerHTML with untrusted data
[ ] Remote content (if any) loaded in separate BrowserView with restricted permissions
[ ] protocol.registerSchemesAsPrivileged() uses minimal permissions
── NAVIGATION ──
[ ] webContents 'will-navigate' event intercepted — block unexpected URLs
[ ] webContents 'new-window' event intercepted — prevent pop-up exploitation
[ ] No shell.openExternal() with unsanitized URLs
── PACKAGING ──
[ ] ASAR archive enabled (protects source from casual inspection)
[ ] No sensitive credentials or API keys bundled in the app
[ ] Code signing configured for both Windows and macOS
[ ] Auto-update uses HTTPS and verifies signatures
Preventing Navigation Hijacking:
win.webContents.on('will-navigate', (event, url) => {
const parsedUrl = new URL(url);
if (parsedUrl.origin !== 'http://localhost:5173') {
event.preventDefault();
console.warn(`Blocked navigation to: ${url}`);
}
});
win.webContents.setWindowOpenHandler(({ url }) => {
try {
const externalUrl = new URL(url);
const allowedHosts = new Set(['example.com', 'docs.example.com']);
if (externalUrl.protocol === 'https:' && allowedHosts.has(externalUrl.hostname)) {
require('electron').shell.openExternal(externalUrl.toString());
} else {
console.warn(`Blocked external URL: ${url}`);
}
} catch {
console.warn(`Rejected invalid external URL: ${url}`);
}
return { action: 'deny' };
});
Custom Protocol Registration (secure):
import { protocol } from 'electron';
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { URL } from 'node:url';
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } },
]);
app.whenReady().then(() => {
protocol.handle('app', async (request) => {
const url = new URL(request.url);
const baseDir = path.resolve(__dirname, '../renderer');
const relativePath = path.normalize(decodeURIComponent(url.pathname).replace(/^[/\\]+/, ''));
const filePath = path.resolve(baseDir, relativePath);
if (!filePath.startsWith(baseDir)) {