electron
Installation
SKILL.md
When to Use
Triggers: When building Electron apps, working with main/renderer processes, IPC communication, or native OS integrations.
Load when: building Electron desktop apps, implementing IPC between main and renderer, handling native OS features, or setting up auto-updates.
Critical Patterns — SECURITY
// ✅ ALWAYS: contextIsolation + NO nodeIntegration
new BrowserWindow({
webPreferences: {
contextIsolation: true, // MANDATORY
nodeIntegration: false, // NEVER enable
preload: path.join(__dirname, 'preload.js'),
sandbox: true, // Recommended
}
});
// ❌ NEVER: nodeIntegration enabled
new BrowserWindow({
webPreferences: {
nodeIntegration: true, // Security VULNERABILITY
}
});
// ❌ NEVER: use remote module (deprecated)
const { remote } = require('electron');
// ❌ NEVER: expose full ipcRenderer
contextBridge.exposeInMainWorld('api', { ipcRenderer }); // DANGEROUS
Code Examples
Main Process — Secure initialization
// main/index.ts
import { app, BrowserWindow } from 'electron';
import path from 'path';
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js'),
},
});
if (process.env.NODE_ENV === 'development') {
win.loadURL('http://localhost:5173');
} else {
win.loadFile(path.join(__dirname, '../renderer/index.html'));
}
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Preload — Secure Context Bridge
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// Define typed channels
type Channels = 'read-file' | 'write-file' | 'open-dialog';
contextBridge.exposeInMainWorld('api', {
// Only expose specific functions, not the full ipcRenderer
readFile: (filePath: string) =>
ipcRenderer.invoke('read-file', filePath),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('write-file', filePath, content),
openDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke('open-dialog', options),
onProgress: (callback: (progress: number) => void) => {
ipcRenderer.on('progress', (_, value) => callback(value));
return () => ipcRenderer.removeAllListeners('progress');
},
});
IPC Handlers in Main
// main/handlers.ts
import { ipcMain, dialog, app } from 'electron';
import fs from 'fs/promises';
ipcMain.handle('read-file', async (_, filePath: string) => {
try {
const content = await fs.readFile(filePath, 'utf-8');
return { success: true, content };
} catch (error) {
return { success: false, error: String(error) };
}
});
ipcMain.handle('write-file', async (_, filePath: string, content: string) => {
await fs.writeFile(filePath, content, 'utf-8');
return { success: true };
});
ipcMain.handle('open-dialog', async (_, options) => {
const result = await dialog.showOpenDialog(options);
return result;
});
React Hook for IPC
// hooks/useFileSystem.ts
import { useState } from 'react';
declare global {
interface Window {
api: {
readFile: (path: string) => Promise<{ success: boolean; content?: string }>;
writeFile: (path: string, content: string) => Promise<{ success: boolean }>;
openDialog: (options: any) => Promise<Electron.OpenDialogReturnValue>;
};
}
}
export function useFileSystem() {
const [isLoading, setIsLoading] = useState(false);
const openAndReadFile = async () => {
setIsLoading(true);
try {
const { canceled, filePaths } = await window.api.openDialog({
properties: ['openFile'],
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (canceled || !filePaths[0]) return null;
const result = await window.api.readFile(filePaths[0]);
return result.success ? result.content : null;
} finally {
setIsLoading(false);
}
};
return { openAndReadFile, isLoading };
}
Auto-Updater
// main/updater.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow } from 'electron';
export function setupAutoUpdater(win: BrowserWindow) {
autoUpdater.autoDownload = false;
autoUpdater.on('update-available', (info) => {
win.webContents.send('update-available', info);
});
autoUpdater.on('download-progress', (progress) => {
win.webContents.send('progress', progress.percent);
});
autoUpdater.on('update-downloaded', () => {
win.webContents.send('update-ready');
});
autoUpdater.checkForUpdates();
}
Anti-Patterns
❌ nodeIntegration: true
// ❌ Allows the renderer to access Node.js — VULNERABILITY
webPreferences: { nodeIntegration: true }
// ✅ Use contextBridge + preload
webPreferences: { contextIsolation: true, preload: '...' }
❌ Expose full ipcRenderer
// ❌ The renderer can listen/send on any channel
contextBridge.exposeInMainWorld('electron', { ipcRenderer });
// ✅ Only expose specific, typed functions
contextBridge.exposeInMainWorld('api', {
readFile: (path: string) => ipcRenderer.invoke('read-file', path),
});
Quick Reference
| Task | Pattern |
|---|---|
| Secure window | contextIsolation: true, nodeIntegration: false |
| Expose API | contextBridge.exposeInMainWorld('api', {...}) |
| Bidirectional IPC | ipcMain.handle() + ipcRenderer.invoke() |
| Unidirectional IPC | ipcRenderer.send() + ipcMain.on() |
| Main→renderer events | win.webContents.send() + ipcRenderer.on() |
| Native dialogs | dialog.showOpenDialog() in main |
| Auto-update | electron-updater |
| Native menu | Menu.buildFromTemplate() |
Rules
- All IPC communication must go through named channels defined in
preload.js— never expose the fullipcRendererobject to the renderer process contextIsolation: trueandnodeIntegration: falseare required security settings; do not relax them without explicit justification- Long-running or blocking operations (file I/O, network) belong in the main process, not the renderer
- Auto-updater events must be handled explicitly; silent failures leave users on outdated versions
- Native OS integrations (menus, trays, notifications) must be set up in the main process lifecycle, not in renderer components