$npx -y skills add jamditis/claude-skills-journalism --skill electron-devElectron desktop application development with React, TypeScript, and Vite. Use when building desktop apps, implementing IPC communication, managing windows/tray, handling PTY terminals, integrating WebRTC/audio, or packaging with electron-builder. Covers patterns from AudioBash,
| 1 | # Electron desktop development |
| 2 | |
| 3 | Patterns and practices for building production-quality Electron applications with React and TypeScript. |
| 4 | |
| 5 | ## Security baseline (Electron 30+) |
| 6 | |
| 7 | Electron's defaults have hardened over the past several releases. As of Electron 28+, `contextIsolation: true` and `sandbox: true` are the defaults for new BrowserWindow instances — most security advice from older guides assumed you had to opt in. You don't anymore; you have to opt OUT, and you should not. |
| 8 | |
| 9 | Set explicitly anyway, so a config drift never weakens the security model: |
| 10 | |
| 11 | ```javascript |
| 12 | const win = new BrowserWindow({ |
| 13 | webPreferences: { |
| 14 | contextIsolation: true, // default since 12, mandatory for any prod app |
| 15 | sandbox: true, // default since 28; renderer runs sandboxed |
| 16 | nodeIntegration: false, // never enable in renderer |
| 17 | webSecurity: true, // never disable |
| 18 | preload: path.join(__dirname, 'preload.cjs') |
| 19 | } |
| 20 | }); |
| 21 | ``` |
| 22 | |
| 23 | Validate every IPC message in main. Don't trust the renderer. |
| 24 | |
| 25 | ### Electron Fuses + ASAR integrity |
| 26 | |
| 27 | Electron Fuses are package-time toggles baked into the binary. The two relevant for security distribution: |
| 28 | |
| 29 | - `EnableEmbeddedAsarIntegrityValidation` — verifies the app.asar hash at runtime against a hash embedded in the binary. Defends against attackers swapping the asar contents post-install. |
| 30 | - `OnlyLoadAppFromAsar` — refuses to load app code from anywhere except the validated asar. |
| 31 | |
| 32 | These are **opt-in**, not default. Enable both for production. Requires `@electron/asar` 3.1.0+ to generate the asar with embeddable integrity. electron-builder configures this via `electronFuses` in the build config; `@electron/fuses` does it programmatically. |
| 33 | |
| 34 | CVE-2023-44402 (ASAR integrity bypass via filetype confusion) was the canonical motivation here — without integrity + only-load-from-asar, an attacker who can modify app files can swap behavior silently. |
| 35 | |
| 36 | ### Common renderer-side risks |
| 37 | |
| 38 | - **Preload script confusion** — only expose narrow, typed surfaces via `contextBridge.exposeInMainWorld`. Never re-export `ipcRenderer` itself; expose specific methods that map to specific channels. |
| 39 | - **`file://` IPC and navigation** — restrict navigation with `webContents.on('will-navigate', e => e.preventDefault())` for windows that shouldn't change URL. Deny `setWindowOpenHandler` requests by default; allow-list specific origins. |
| 40 | - **`shell.openExternal` with user input** — validate the URL scheme before opening. An attacker-controlled `file://` or `javascript:` URL hands them code execution. |
| 41 | |
| 42 | ## Architecture patterns |
| 43 | |
| 44 | ### Project structure |
| 45 | ``` |
| 46 | app/ |
| 47 | ├── electron/ |
| 48 | │ ├── main.cjs # Main process (CommonJS required) |
| 49 | │ ├── preload.cjs # Context bridge for secure IPC |
| 50 | │ └── server.cjs # Optional: WebSocket/HTTP server |
| 51 | ├── src/ |
| 52 | │ ├── components/ # React components |
| 53 | │ ├── services/ # Business logic (API clients, Firebase) |
| 54 | │ ├── utils/ # Utilities (audio, formatting) |
| 55 | │ ├── types.ts # TypeScript interfaces |
| 56 | │ ├── App.tsx # Root component |
| 57 | │ └── index.tsx # React entry |
| 58 | ├── assets/ # Icons, sounds, images |
| 59 | ├── package.json |
| 60 | ├── vite.config.ts |
| 61 | └── electron-builder.yml # Build configuration |
| 62 | ``` |
| 63 | |
| 64 | ### IPC communication pattern |
| 65 | |
| 66 | **Main process (main.cjs):** |
| 67 | ```javascript |
| 68 | const { ipcMain } = require('electron'); |
| 69 | |
| 70 | // Handle async requests from renderer |
| 71 | ipcMain.handle('action-name', async (event, args) => { |
| 72 | try { |
| 73 | const result = await someAsyncOperation(args); |
| 74 | return { success: true, data: result }; |
| 75 | } catch (error) { |
| 76 | return { success: false, error: error.message }; |
| 77 | } |
| 78 | }); |
| 79 | |
| 80 | // Send data to renderer |
| 81 | mainWindow.webContents.send('event-name', data); |
| 82 | ``` |
| 83 | |
| 84 | **Preload script (preload.cjs):** |
| 85 | ```javascript |
| 86 | const { contextBridge, ipcRenderer } = require('electron'); |
| 87 | |
| 88 | contextBridge.exposeInMainWorld('electron', { |
| 89 | actionName: (args) => ipcRenderer.invoke('action-name', args), |
| 90 | onEventName: (callback) => { |
| 91 | const handler = (event, data) => callback(data); |
| 92 | ipcRenderer.on('event-name', handler); |
| 93 | return () => ipcRenderer.removeListener('event-name', handler); |
| 94 | } |
| 95 | }); |
| 96 | ``` |
| 97 | |
| 98 | **Renderer (React):** |
| 99 | ```typescript |
| 100 | const result = await window.electron.actionName(args); |
| 101 | |
| 102 | useEffect(() => { |
| 103 | return window.electron.onEventName((data) => { |
| 104 | setState(data); |
| 105 | }); |
| 106 | }, []); |
| 107 | ``` |
| 108 | |
| 109 | ## System tray integration |
| 110 | |
| 111 | ```javascript |
| 112 | const { Tray, Menu, nativeImage } = require('electron'); |
| 113 | |
| 114 | let tray = null; |
| 115 | |
| 116 | function createTray() { |
| 117 | const icon = nativeImage.createFromPath(path.join(__dirname, '../assets/tray-icon.png')); |
| 118 | tray = n |