Electron 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, Yap, and Pisscord projects.
npx skills add https://github.com/jamditis/claude-skills-journalism --skill electron-dev
Patterns and practices for building production-quality Electron applications with React and TypeScript.
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.
Set explicitly anyway, so a config drift never weakens the security model:
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true, // default since 12, mandatory for any prod app
sandbox: true, // default since 28; renderer runs sandboxed
nodeIntegration: false, // never enable in renderer
webSecurity: true, // never disable
preload: path.join(__dirname, 'preload.cjs')
}
});
Validate every IPC message in main. Don't trust the renderer.
Electron Fuses are package-time toggles baked into the binary. The two relevant for security distribution:
EnableEmbeddedAsarIntegrityValidation — verifies the app.asar hash at runtime against a hash embedded in the binary. Defends against attackers swapping the asar contents post-install.OnlyLoadAppFromAsar — refuses to load app code from anywhere except the validated asar.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.
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.
contextBridge.exposeInMainWorld. Never re-export ipcRenderer itself; expose specific methods that map to specific channels.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.shell.openExternal with user input — validate the URL scheme before opening. An attacker-controlled file:// or javascript: URL hands them code execution.app/
├── electron/
│ ├── main.cjs # Main process (CommonJS required)
│ ├── preload.cjs # Context bridge for secure IPC
│ └── server.cjs # Optional: WebSocket/HTTP server
├── src/
│ ├── components/ # React components
│ ├── services/ # Business logic (API clients, Firebase)
│ ├── utils/ # Utilities (audio, formatting)
│ ├── types.ts # TypeScript interfaces
│ ├── App.tsx # Root component
│ └── index.tsx # React entry
├── assets/ # Icons, sounds, images
├── package.json
├── vite.config.ts
└── electron-builder.yml # Build configuration
Main process (main.cjs):
const { ipcMain } = require('electron');
// Handle async requests from renderer
ipcMain.handle('action-name', async (event, args) => {
try {
const result = await someAsyncOperation(args);
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
// Send data to renderer
mainWindow.webContents.send('event-name', data);
Preload script (preload.cjs):
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
actionName: (args) => ipcRenderer.invoke('action-name', args),
onEventName: (callback) => {
const handler = (event, data) => callback(data);
ipcRenderer.on('event-name', handler);
return () => ipcRenderer.removeListener('event-name', handler);
}
});
Renderer (React):
const result = await window.electron.actionName(args);
useEffect(() => {
return window.electron.onEventName((data) => {
setState(data);
});
}, []);
const { Tray, Menu, nativeImage } = require('electron');
let tray = null;
function createTray() {
const icon = nativeImage.createFromPath(path.join(__dirname, '../assets/tray-icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip('App Name');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Show', click: () => mainWindow.show() },
{ label: 'Quit', click: () => app.quit() }
]));
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
}
// Hide to tray instead of closing
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
});
const { globalShortcut } = require('electron');
app.whenReady().then(() => {
// Register with conflict detection
const registered = globalShortcut.register('Alt+S', () => {
mainWindow.webContents.send('shortcut-triggered', 'toggle-recording');
});
if (!registered) {
console.error('Shortcut registration failed - conflict detected');
}
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});
const pty = require('node-pty');
const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME,
env: process.env
});
ptyProcess.onData((data) => {
mainWindow.webContents.send('terminal-data', { tabId, data });
});
ipcMain.on('terminal-write', (event, { tabId, data }) => {
ptyProcess.write(data);
});
ipcMain.on('terminal-resize', (event, { tabId, cols, rows }) => {
ptyProcess.resize(cols, rows);
});
// Request microphone access
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// Record audio
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
const chunks: Blob[] = [];
mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
const base64 = await blobToBase64(blob);
// Send to transcription API
};
mediaRecorder.start();
// Later: mediaRecorder.stop();
import Peer from 'peerjs';
const peer = new Peer(userId, {
host: 'peerjs-server.com',
port: 443,
secure: true
});
// Answer incoming calls
peer.on('call', (call) => {
call.answer(localStream);
call.on('stream', (remoteStream) => {
audioElement.srcObject = remoteStream;
});
});
// Make outgoing calls
const call = peer.call(remoteUserId, localStream);
call.on('stream', (remoteStream) => {
audioElement.srcObject = remoteStream;
});
// Screen sharing via replaceTrack (no renegotiation)
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const videoTrack = screenStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(videoTrack);
appId: com.yourname.appname
productName: AppName
directories:
output: release
win:
target:
- target: nsis
arch: [x64]
icon: assets/icon.ico
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
installerIcon: assets/icon.ico
uninstallerIcon: assets/icon.ico
mac:
target:
- target: dmg
arch: [x64, arm64]
icon: assets/icon.icns
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize:
teamId: YOUR_APPLE_TEAM_ID
linux:
target:
- target: AppImage
arch: [x64]
icon: assets/icon.png
publish:
provider: github
owner: username
repo: repo-name
extraResources:
- from: "node_modules/node-pty/build/Release/"
to: "node-pty/"
filter: ["*.node"]
macOS notarization is required for distribution outside the App Store; Gatekeeper blocks unnotarized apps on first launch. Set the env vars APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID (or use an App Store Connect API key) before running npm run package. electron-builder ≥ 24.13 handles notarization natively via the mac.notarize field; older versions require the electron-notarize afterSign hook.
For Windows, code signing with an EV cert is increasingly necessary to avoid SmartScreen warnings. electron-builder reads CSC_LINK (PFX) and CSC_KEY_PASSWORD env vars.
Stale closures in callbacks:
// Problem: State is stale in async callbacks
const [state, setState] = useState(initialValue);
peer.on('call', () => {
console.log(state); // Always shows initialValue
});
// Solution: Use refs for async callback access
const stateRef = useRef(state);
useEffect(() => { stateRef.current = state; }, [state]);
peer.on('call', () => {
console.log(stateRef.current); // Current value
});
Context isolation security:
ipcRenderer directly to renderercontextBridge.exposeInMainWorld()BrowserView is deprecated — use WebContentsView:
BrowserView was deprecated in Electron 30 (April 2024) and the underlying implementation has been replaced. BrowserView still works as a compatibility shim over WebContentsView, but new code should target WebContentsView directly. The constructors take the same webPreferences shape, so the migration is mostly mechanical. The differences worth knowing:
WebContentsView is added via win.contentView.addChildView(view) instead of win.addBrowserView(view)view.setBounds({x, y, width, height}) — no setAutoResize. You wire your own resize handlers if you want auto-resize.addChildView calls; removeChildView then re-addChildView to bring forward.const { WebContentsView } = require('electron');
const view = new WebContentsView({
webPreferences: { contextIsolation: true, sandbox: true }
});
view.webContents.loadURL('https://example.com');
mainWindow.contentView.addChildView(view);
view.setBounds({ x: 0, y: 80, width: 800, height: 520 });
See the official BrowserView → WebContentsView migration guide for edge cases (popups, devtools, focus management).
Cross-platform shell detection:
const shell = process.platform === 'win32'
? 'powershell.exe'
: process.env.SHELL || '/bin/bash';
const shellArgs = process.platform === 'win32'
? ['-NoLogo']
: [];
# Development (hot reload)
npm run electron:dev
# Production build
npm run electron:build
# Run built app locally
npx electron dist/
# Package for distribution
npm run package
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take jamditis/electron-dev from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.