Skills desktop-framework-electron
Electron process architecture, IPC patterns, preload security, native APIs, packaging and distribution
git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/dist/plugins/desktop-framework-electron/skills/desktop-framework-electron" ~/.claude/skills/agents-inc-skills-desktop-framework-electron && rm -rf "$T"
dist/plugins/desktop-framework-electron/skills/desktop-framework-electron/SKILL.mdElectron Desktop Applications
Quick Guide: Electron apps run two process types: a main process (Node.js, manages windows and system APIs) and renderer processes (Chromium, one per window). All communication between them flows through IPC via a preload script that uses
to expose a minimal, typed API surface. Never disablecontextBridge. Never enablecontextIsolationin renderers. Package with Electron Forge or Electron Builder. Auto-update vianodeIntegration(Squirrel on macOS/Windows) orautoUpdaterfor all platforms.electron-updater
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST keep
(the default) -- disabling it exposes the entire preload scope to untrusted renderer code)contextIsolation: true
(You MUST use
in preload scripts -- never expose contextBridge.exposeInMainWorld()
directly)ipcRenderer
(You MUST NOT enable
in any BrowserWindow -- it gives renderers full Node.js access, which is a critical security vulnerability)nodeIntegration: true
(You MUST validate and sanitize ALL data received via IPC in the main process -- treat renderer messages as untrusted input)
(You MUST use
/ ipcMain.handle()
for request-response IPC -- avoid ipcRenderer.invoke()
which blocks the renderer)sendSync
(You MUST NOT load remote URLs with
or disabled nodeIntegration
-- this is equivalent to giving the remote site full system access)contextIsolation
</critical_requirements>
Auto-detection: Electron, electron, BrowserWindow, ipcMain, ipcRenderer, contextBridge, preload, webPreferences, electron-builder, electron-forge, app.whenReady, electronAPI, mainWindow, autoUpdater, nativeTheme, safeStorage, Tray, Menu, dialog, protocol, shell
When to use:
- Building cross-platform desktop applications
- Configuring main process / renderer process architecture
- Setting up secure IPC communication patterns
- Integrating with native OS features (tray, menus, dialogs, notifications, file system)
- Packaging and distributing desktop applications
- Implementing auto-update functionality
- Registering custom protocol handlers / deep links
When NOT to use:
- Choosing a UI framework for the renderer (use the appropriate web framework skill)
- Styling the renderer UI (use the appropriate styling skill)
- Server-side or backend logic not related to the main process
- Mobile applications (Electron is desktop-only)
- CLI tools that do not need a GUI
<patterns>
Key Patterns
Pattern 1: Secure BrowserWindow Creation
Every BrowserWindow must use a preload script and rely on the secure defaults:
contextIsolation: true, sandbox: true, nodeIntegration: false.
const mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, "preload.js"), // contextIsolation: true -- default since Electron 12 // sandbox: true -- default since Electron 20 // nodeIntegration: false -- default since Electron 5 }, });
Key point: Never override the security defaults. The preload script is the ONLY bridge between main and renderer. See examples/core.md.
Pattern 2: Preload with contextBridge
The preload script exposes a narrow, explicitly typed API to the renderer. Never expose
ipcRenderer directly.
// preload.js const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { readFile: (filePath) => ipcRenderer.invoke("read-file", filePath), onUpdateAvailable: (callback) => { ipcRenderer.on("update-available", (_event, data) => callback(data)); }, });
Key point: Each exposed method wraps a single IPC channel. The renderer calls
window.electronAPI.readFile(path) with no knowledge of IPC internals. See examples/core.md.
Pattern 3: IPC Request-Response (invoke/handle)
Use
ipcMain.handle() in main and ipcRenderer.invoke() in preload for async two-way communication.
// main process ipcMain.handle("read-file", async (_event, filePath) => { const content = await fs.readFile(filePath, "utf-8"); return { success: true, content }; });
Key point:
handle/invoke returns a Promise. Always validate filePath in the handler -- never trust renderer input. See examples/ipc.md for all IPC patterns.
Pattern 4: Main-to-Renderer Messages
Use
webContents.send() from main and listen in the preload with a callback pattern.
// main: send to specific window mainWindow.webContents.send("update-progress", { percent: 45 }); // preload: expose listener onUpdateProgress: (callback) => { ipcRenderer.on("update-progress", (_event, data) => callback(data)); },
Key point: The renderer cannot pull from main -- main must push. Always scope listeners to specific channels. See examples/ipc.md.
Pattern 5: App Lifecycle
The main process manages the app lifecycle with platform-specific conventions.
app.whenReady().then(() => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });
Key point: macOS apps stay alive when all windows close (
window-all-closed should not quit). The activate event recreates a window when the dock icon is clicked. See examples/core.md.
Pattern 6: Native OS Integration
Electron exposes native APIs for dialogs, menus, tray icons, notifications, and more -- all accessed from the main process.
const { dialog } = require("electron"); const result = await dialog.showOpenDialog(mainWindow, { properties: ["openFile", "multiSelections"], filters: [{ name: "Documents", extensions: ["txt", "md", "json"] }], });
Key point: Native dialogs are modal to a window when passed
mainWindow as the first argument. See examples/native-apis.md.
Pattern 7: Custom Protocol / Deep Linking
Register your app to handle
myapp:// URLs for deep linking from browsers or other apps.
if (process.defaultApp) { app.setAsDefaultProtocolClient("myapp", process.execPath, [ path.resolve(process.argv[1]), ]); } else { app.setAsDefaultProtocolClient("myapp"); }
Key point: In development (
process.defaultApp is true), you must pass the script path as an argument. On macOS, handle the open-url event on the app object. On Windows/Linux, handle via second-instance event. See examples/native-apis.md.
</patterns>
<decision_framework>
Decision Framework
IPC Pattern Selection
Which IPC pattern? +-- Renderer needs a response from main? | +-- YES --> ipcMain.handle() + ipcRenderer.invoke() +-- Renderer sends data, no response needed? | +-- YES --> ipcMain.on() + ipcRenderer.send() +-- Main needs to push data to renderer? | +-- YES --> webContents.send() + ipcRenderer.on() (in preload) +-- Two renderers need to communicate? | +-- YES --> Route through main process (never direct renderer-to-renderer) +-- High-frequency data transfer (streaming)? +-- YES --> MessageChannelMain / MessagePort pair
Window Architecture
How many windows? +-- Single window app? | +-- One BrowserWindow, one preload script +-- Multi-window (e.g., preferences, about)? | +-- Separate BrowserWindow per view, each with its own preload +-- Frameless / custom title bar? | +-- frame: false + custom drag regions via CSS (-webkit-app-region: drag) +-- Persistent background work? +-- Use a hidden BrowserWindow or utilityProcess (Electron 22+)
</decision_framework>
Detailed resources:
- examples/core.md - App lifecycle, BrowserWindow, preload, contextBridge fundamentals
- examples/ipc.md - All IPC patterns: invoke/handle, send/on, main-to-renderer, MessagePort
- examples/security.md - Security hardening, CSP, permission handlers, safe defaults
- examples/native-apis.md - Dialogs, menus, tray, notifications, protocol handlers, auto-updater
- examples/packaging.md - Electron Forge, Electron Builder, code signing, distribution
- reference.md - API quick-reference tables, version history, security checklist
<red_flags>
RED FLAGS
Critical Security Issues:
- Disabling
(contextIsolation
) -- exposes preload globals to renderercontextIsolation: false - Enabling
-- gives renderer full Node.js access (fs, child_process, etc.)nodeIntegration: true - Exposing
directly viaipcRenderer
instead of wrapping individual channelscontextBridge - Loading remote/untrusted URLs without
sandbox: true - Disabling
in production (webSecurity
disables same-origin policy)webSecurity: false - Not validating IPC arguments in main process handlers (path traversal, injection attacks)
- Using
with unvalidated URLs (can execute arbitrary commands)shell.openExternal()
Architecture Issues:
- Using
-- blocks the renderer process, causes UI freezesipcRenderer.sendSync() - Putting business logic in the renderer instead of the main process
- Direct renderer-to-renderer communication (bypassing main)
- Using
module (removed in Electron 14+, was a security and performance hazard)remote - Creating BrowserWindows from the renderer process
- Not handling
per-platform (macOS apps should not quit)window-all-closed
Packaging Issues:
- Shipping
in production builds (bloated app size)devDependencies - Not code-signing the application (OS warnings, auto-update failures on macOS)
- Bundling
without pruning or using ASAR archivenode_modules - Hardcoding absolute paths that differ between dev and packaged environments
- Using
in renderer code (unavailable in sandboxed renderers)__dirname
Common Mistakes:
- Forgetting
-- APIs are unavailable before theapp.whenReady()
eventready - Not re-creating window on
event (macOS dock click does nothing)activate - Using
in renderer scripts loaded viarequire()
tags (not available in sandboxed renderers)<script> - Setting
for iframes loading external contentnodeIntegrationInSubFrames: true - Not setting proper
headers for renderer HTMLContent-Security-Policy
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST keep
(the default) -- disabling it exposes the entire preload scope to untrusted renderer code)contextIsolation: true
(You MUST use
in preload scripts -- never expose contextBridge.exposeInMainWorld()
directly)ipcRenderer
(You MUST NOT enable
in any BrowserWindow -- it gives renderers full Node.js access, which is a critical security vulnerability)nodeIntegration: true
(You MUST validate and sanitize ALL data received via IPC in the main process -- treat renderer messages as untrusted input)
(You MUST use
/ ipcMain.handle()
for request-response IPC -- avoid ipcRenderer.invoke()
which blocks the renderer)sendSync
Failure to follow these rules will create severe security vulnerabilities or broken desktop applications.
</critical_reminders>