agents-inc/skills/src/skills/desktop-ipc-electron/SKILL.md
desktop-ipc-electron
Type-safe Electron IPC patterns with typed channels, electron-trpc, MessagePort, and utility process communication
- Source repository stars
- 23
- Declared platforms
- 0
- Static risk flags
- 1
- Last source update
- 2026-08-09
- Source checked
- 2026-08-28
Decision brief
What it does: where it fits
Quick Guide: All Electron IPC flows through a preload script using contextBridge.exposeInMainWorld(). Make it type-safe by defining a shared channel map that constrains channel names, payloads, and return types across main, preload, and renderer. For end-to-end type safety with…
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/agents-inc/skills --skill "src/skills/desktop-ipc-electron"Inspect the Agent Skill "desktop-ipc-electron" from https://github.com/agents-inc/skills/blob/81d43a51211aca12c85dcc16085fa99014ec548e/src/skills/desktop-ipc-electron/SKILL.md at commit 81d43a51211aca12c85dcc16085fa99014ec548e. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Pattern 6: Utility Process IPC
Use utilityProcess.fork() for CPU-intensive work. Communication flows through parentPort.
Use utilityProcess.fork() for CPU-intensive work. Communication flows through parentPort.Key points: utility processes have full Node.js access, communicate via parentPort.postMessage(), and should be used instead of childprocess.fork() in Electron apps.See examples/message-ports.md for typed utility process communication. - 02
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)
Adding type safety to Electron IPC communicationSetting up electron-trpc for end-to-end typed IPCDefining shared channel/payload types between main and renderer - 03
Philosophy
Electron IPC is stringly typed by default -- channel names are plain strings, payloads are any, and there is no compile-time guarantee that the main process handler matches what the renderer sends. Type-safe IPC solves this by defining a single source of truth for channel names,…
Shared channel map + typed wrappers (DIY) -- define an IpcChannelMap interface, create thin typed wrappers around ipcMain/ipcRenderer. Zero dependencies, full control.electron-trpc (library) -- tRPC over Electron IPC. Define a router in main with Zod-validated procedures, get a fully typed client in the renderer. Best DX for complex apps.MessagePort with typed messages -- for high-throughput streaming or renderer-to-renderer communication where standard IPC overhead matters. - 04
Core Patterns
Define all channel names, argument types, and return types in a single shared file. Both main and renderer import from this file.
Define all channel names, argument types, and return types in a single shared file. Both main and renderer import from this file.Why good: Single source of truth for all IPC contracts, TypeScript catches mismatches at compile time, channel names are autocompletedSee examples/core.md for typed wrappers that consume this map. - 05
Pattern 1: Shared IPC Channel Map
Define all channel names, argument types, and return types in a single shared file. Both main and renderer import from this file.
Define all channel names, argument types, and return types in a single shared file. Both main and renderer import from this file.Why good: Single source of truth for all IPC contracts, TypeScript catches mismatches at compile time, channel names are autocompletedSee examples/core.md for typed wrappers that consume this map.
Permission review
Static risk signals and limitations
Reads files
The documentation asks the agent to read local files, directories, or repositories.
readFile: t.procedureReads files
The documentation asks the agent to read local files, directories, or repositories.
const content = await fs.readFile(input.path, "utf-8");Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 92/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 23 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- agents-inc/skills
- Skill path
- src/skills/desktop-ipc-electron/SKILL.md
- Commit
- 81d43a51211aca12c85dcc16085fa99014ec548e
- License
- MIT
- Collected
- 2026-08-28
- Default branch
- main
View the original SKILL.md
Electron Type-Safe IPC Patterns
Quick Guide: All Electron IPC flows through a preload script using
contextBridge.exposeInMainWorld(). Make it type-safe by defining a shared channel map that constrains channel names, payloads, and return types across main, preload, and renderer. For end-to-end type safety with minimal boilerplate, useelectron-trpc(tRPC over IPC). For high-throughput streaming or renderer-to-renderer communication, useMessageChannelMain/MessagePort. For CPU-intensive background work, useutilityProcesswithparentPort. Always validate IPC input in the main process -- treat renderer messages as untrusted.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST validate and sanitize ALL data received via IPC in the main process -- treat renderer messages as untrusted input)
(You MUST use contextBridge.exposeInMainWorld() in preload scripts -- never expose ipcRenderer directly)
(You MUST define IPC channel names and payload types in a single shared file -- never use untyped string literals for channel names)
(You MUST use ipcMain.handle() / ipcRenderer.invoke() for request-response IPC -- sendSync blocks the renderer)
(You MUST clean up IPC listeners when components unmount or windows close -- listener leaks cause memory issues and duplicate handlers)
</critical_requirements>
Auto-detection: Electron IPC, ipcMain, ipcRenderer, contextBridge, preload, type-safe IPC, electron-trpc, ipcLink, createIPCHandler, exposeElectronTRPC, MessageChannelMain, MessagePortMain, MessagePort, utilityProcess, parentPort, typed channels, IPC channel map, postMessage, webContents.send, ipcMain.handle, ipcRenderer.invoke
When to use:
- Adding type safety to Electron IPC communication
- Setting up electron-trpc for end-to-end typed IPC
- Defining shared channel/payload types between main and renderer
- Building typed preload APIs with contextBridge
- Using MessagePort for high-throughput or renderer-to-renderer communication
- Implementing utility process IPC for background tasks
- Validating and sanitizing IPC input in main process handlers
When NOT to use:
- Choosing a UI framework for the renderer (use the appropriate framework skill)
- General Electron app setup, packaging, or native APIs (use the Electron framework skill)
- Simple IPC that does not need type safety beyond basic JavaScript
Key patterns covered:
- Shared IPC channel map with typed payloads and return types
- Typed preload API via contextBridge with declaration augmentation
- electron-trpc for end-to-end type safety (queries, mutations, subscriptions)
- Request-response (
handle/invoke) with typed wrappers - Fire-and-forget (
on/send) with typed channels - Main-to-renderer push (
webContents.send) with typed events - MessagePort for high-throughput and renderer-to-renderer communication
- Utility process IPC with
parentPortand MessagePort transfer - IPC input validation and channel allowlisting
Detailed Resources:
- examples/core.md - Shared channel map, typed preload, typed wrappers, declaration augmentation
- examples/electron-trpc.md - electron-trpc setup, queries, mutations, subscriptions
- examples/message-ports.md - MessagePort patterns, renderer-to-renderer, utility process IPC
- reference.md - IPC method quick reference, decision framework, security checklist
Philosophy
Electron IPC is stringly typed by default -- channel names are plain strings, payloads are any, and there is no compile-time guarantee that the main process handler matches what the renderer sends. Type-safe IPC solves this by defining a single source of truth for channel names, argument types, and return types, then threading those types through typed wrapper functions.
Three levels of type safety, pick one:
- Shared channel map + typed wrappers (DIY) -- define an
IpcChannelMapinterface, create thin typed wrappers aroundipcMain/ipcRenderer. Zero dependencies, full control. - electron-trpc (library) -- tRPC over Electron IPC. Define a router in main with Zod-validated procedures, get a fully typed client in the renderer. Best DX for complex apps.
- MessagePort with typed messages -- for high-throughput streaming or renderer-to-renderer communication where standard IPC overhead matters.
When to use each:
- Shared channel map: Most apps. Simple, no dependencies, covers
handle/invoke,send/on, andwebContents.send. - electron-trpc: Apps with many IPC endpoints, complex input validation, or subscription needs. Worth the dependency when you have 10+ IPC channels.
- MessagePort: Real-time data feeds, large binary transfers, or direct renderer-to-renderer communication. Not a replacement for standard IPC -- an addition for specific high-throughput needs.
When NOT to use type-safe IPC:
- Prototyping where speed matters more than safety
- Apps with 1-2 trivial IPC calls where the overhead of typed infrastructure is not justified
Core Patterns
Pattern 1: Shared IPC Channel Map
Define all channel names, argument types, and return types in a single shared file. Both main and renderer import from this file.
// shared/ipc-channels.ts
export interface IpcHandleChannels {
"file:read": (filePath: string) => { content: string };
"file:write": (filePath: string, content: string) => { success: boolean };
"dialog:open": (options: OpenDialogOptions) => string | null;
"app:version": () => string;
}
export interface IpcSendChannels {
"analytics:track": [eventName: string, metadata: Record<string, unknown>];
"log:error": [message: string, stack?: string];
}
export interface IpcMainToRendererChannels {
"update:progress": { percent: number; message: string };
"update:available": { version: string };
"theme:changed": "light" | "dark";
}
Why good: Single source of truth for all IPC contracts, TypeScript catches mismatches at compile time, channel names are autocompleted
See examples/core.md for typed wrappers that consume this map.
Pattern 2: Typed Preload with contextBridge
Build a typed preload API from the channel map, then augment window so the renderer gets full autocompletion.
// preload.ts
import { contextBridge, ipcRenderer } from "electron";
import type {
IpcHandleChannels,
IpcSendChannels,
} from "../shared/ipc-channels";
type ElectronAPI = {
invoke: <C extends keyof IpcHandleChannels>(
channel: C,
...args: Parameters<IpcHandleChannels[C]>
) => Promise<ReturnType<IpcHandleChannels[C]>>;
send: <C extends keyof IpcSendChannels>(
channel: C,
...args: IpcSendChannels[C]
) => void;
on: (channel: string, callback: (...args: unknown[]) => void) => () => void;
};
contextBridge.exposeInMainWorld("electronAPI", {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
on: (channel, callback) => {
const listener = (_event: unknown, ...args: unknown[]) => callback(...args);
ipcRenderer.on(channel, listener);
return () => ipcRenderer.removeListener(channel, listener);
},
} satisfies ElectronAPI);
// shared/electron-api.d.ts -- augment window for renderer autocompletion
import type { ElectronAPI } from "../preload";
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
Why good: renderer gets autocomplete on channel names and typed payloads, on returns an unsubscribe function for easy cleanup
See examples/core.md for the full pattern with main process typed handlers.
Pattern 3: electron-trpc for End-to-End Type Safety
For apps with many IPC endpoints, electron-trpc provides the best developer experience by leveraging tRPC's router pattern.
// main/router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create({ isServer: true });
export const router = t.router({
readFile: t.procedure
.input(z.object({ path: z.string() }))
.query(async ({ input }) => {
const content = await fs.readFile(input.path, "utf-8");
return { content };
}),
saveSettings: t.procedure
.input(z.object({ theme: z.enum(["light", "dark"]) }))
.mutation(async ({ input }) => {
await saveToStore(input);
return { success: true };
}),
});
export type AppRouter = typeof router;
// renderer/client.ts
import { createTRPCProxyClient } from "@trpc/client";
import { ipcLink } from "electron-trpc/renderer";
import type { AppRouter } from "../main/router";
export const trpc = createTRPCProxyClient<AppRouter>({
links: [ipcLink()],
});
// Fully typed -- autocomplete on procedures, typed input/output
const result = await trpc.readFile.query({ path: "/some/file.txt" });
Why good: Zod validates input at runtime in main, TypeScript validates at compile time in renderer, adding a new procedure auto-surfaces in the client
See examples/electron-trpc.md for full setup including preload, subscriptions, and context patterns.
Pattern 4: IPC Input Validation
Always validate arguments in main process handlers. The renderer can be compromised via XSS -- main process handlers have full Node.js access.
// main/handlers.ts
const ALLOWED_EXTENSIONS = new Set([".txt", ".md", ".json"]);
const MAX_CONTENT_LENGTH = 10 * 1024 * 1024; // 10MB
ipcMain.handle("file:read", async (_event, filePath: unknown) => {
// Type check
if (typeof filePath !== "string") {
throw new Error("filePath must be a string");
}
// Path traversal prevention
const resolved = path.resolve(app.getPath("userData"), filePath);
if (!resolved.startsWith(app.getPath("userData"))) {
throw new Error("Access denied: path outside allowed directory");
}
// Extension allowlist
const ext = path.extname(resolved);
if (!ALLOWED_EXTENSIONS.has(ext)) {
throw new Error(`File type not allowed: ${ext}`);
}
return { content: await fs.readFile(resolved, "utf-8") };
});
Why good: validates type, prevents path traversal, restricts file extensions, uses named constants
See examples/core.md for a channel validation middleware pattern.
Pattern 5: MessagePort for High-Throughput Communication
Use MessageChannelMain for streaming data, large transfers, or direct renderer-to-renderer communication.
// main.ts -- create a port pair and send one end to renderer
import { MessageChannelMain } from "electron";
function createDataChannel(win: BrowserWindow): MessagePortMain {
const { port1, port2 } = new MessageChannelMain();
win.webContents.postMessage("port-transfer", null, [port2]);
port1.start();
return port1;
}
// preload.ts -- receive port and expose to renderer
ipcRenderer.on("port-transfer", (event) => {
const [port] = event.ports;
contextBridge.exposeInMainWorld("dataPort", port);
});
Key points: ports are transferred via postMessage (not send/invoke), port.start() must be called on the main side, renderer side auto-starts when adding a message listener.
See examples/message-ports.md for renderer-to-renderer and utility process patterns.
Pattern 6: Utility Process IPC
Use utilityProcess.fork() for CPU-intensive work. Communication flows through parentPort.
// main.ts
import { utilityProcess } from "electron";
const worker = utilityProcess.fork(path.join(__dirname, "worker.js"));
worker.postMessage({ type: "process-data", payload: largeDataset });
worker.on("message", (result) => {
mainWindow.webContents.send("processing-complete", result);
});
// worker.ts (runs in utility process)
process.parentPort.on("message", (event) => {
const { type, payload } = event.data;
if (type === "process-data") {
const result = heavyComputation(payload);
process.parentPort.postMessage({ type: "result", data: result });
}
});
Key points: utility processes have full Node.js access, communicate via parentPort.postMessage(), and should be used instead of child_process.fork() in Electron apps.
See examples/message-ports.md for typed utility process communication.
<decision_framework>
Decision Framework
Which Type Safety Approach?
How many IPC channels does the app have?
+-- 1-5 channels?
| +-- Shared channel map + typed wrappers (no dependencies)
+-- 5-20 channels?
| +-- Shared channel map works, but electron-trpc adds value
+-- 20+ channels or complex validation?
| +-- electron-trpc (Zod validation + typed client)
+-- Need subscriptions / real-time updates?
+-- electron-trpc subscriptions OR MessagePort
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 --> MessagePort (set up via main process)
High-frequency streaming data?
+-- YES --> MessagePort (avoids per-message IPC overhead)
CPU-intensive background work?
+-- YES --> utilityProcess.fork() + parentPort
</decision_framework>
<red_flags>
RED FLAGS
Critical Security Issues:
- Exposing
ipcRendererdirectly viacontextBridgeinstead of wrapping specific channels -- gives renderer full IPC access - Not validating IPC arguments in main process handlers -- path traversal, injection, privilege escalation
- Using
ipcRenderer.sendSync()-- blocks the entire renderer process, causes UI freezes - Accepting arbitrary file paths from renderer without resolving and checking boundaries
Type Safety Issues:
- Using string literals for channel names without a shared type map -- typos become runtime bugs
- Defining IPC types separately in main and renderer -- they will drift apart
- Not augmenting
windowtype with the preload API -- renderer code has no autocompletion - Using
anyfor IPC payloads -- defeats the purpose of typed IPC
Architecture Issues:
- Not cleaning up
ipcRenderer.onlisteners when components unmount -- causes memory leaks and duplicate handlers - Direct renderer-to-renderer communication without going through main or MessagePort -- not possible in Electron
- Putting business logic in the renderer that should live in main
- Using
child_process.fork()instead ofutilityProcess.fork()in Electron apps
electron-trpc Gotchas:
- Forgetting
exposeElectronTRPC()in the preload script -- client silently fails - Not using a transformer (e.g., SuperJSON) when procedures return
Date,Map, orSet-- serialization loses type information - Subscriptions auto-cancel on window navigation -- resubscribe if the page is a SPA that does not reload
- Custom error classes lose properties during IPC serialization -- use plain error objects or error codes
MessagePort Gotchas:
- Ports must be transferred via
postMessage, notsendorinvoke-- the transfer list is a third argument - Main side must call
port.start()explicitly -- forgetting this means no messages flow port.closeevent fires when the remote end is garbage collected -- handle gracefullySharedArrayBufferis NOT reliably supported in Electron across process boundaries due to cross-origin isolation limitations
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST validate and sanitize ALL data received via IPC in the main process -- treat renderer messages as untrusted input)
(You MUST use contextBridge.exposeInMainWorld() in preload scripts -- never expose ipcRenderer directly)
(You MUST define IPC channel names and payload types in a single shared file -- never use untyped string literals for channel names)
(You MUST use ipcMain.handle() / ipcRenderer.invoke() for request-response IPC -- sendSync blocks the renderer)
(You MUST clean up IPC listeners when components unmount or windows close -- listener leaks cause memory issues and duplicate handlers)
Failure to follow these rules will create security vulnerabilities, type mismatches across process boundaries, and memory leaks.
</critical_reminders>
Frequently asked questions
What to verify before installation and use
What does the desktop-ipc-electron source document cover?
Quick Guide: All Electron IPC flows through a preload script using contextBridge.exposeInMainWorld(). Make it type-safe by defining a shared channel map that constrains channel names, payloads, and return types across main, preload, and renderer. For end-to-end type safety with…
How do I install desktop-ipc-electron?
The source record exposes this install command: npx skills add https://github.com/agents-inc/skills --skill "src/skills/desktop-ipc-electron". Inspect the command and pinned source before running it.
Which permission-related actions were detected?
Static rules flagged read-files in the source; the page lists the matching lines and excerpts.