本文へ移動
cccskills
無料GitHub で公開

desktop-ipc-electron

Type-safe Electron IPC patterns with typed channels, electron-trpc, MessagePort, and utility process communication

インストール方法を見る

含まれるファイル(6)

  • SKILL.md16.7 KB
  • examples/core.md10.0 KB
  • examples/electron-trpc.md9.1 KB
  • examples/message-ports.md9.8 KB
  • metadata.yaml633 B
  • reference.md4.6 KB

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, use electron-trpc (tRPC over IPC). For high-throughput streaming or renderer-to-renderer communication, use MessageChannelMain/MessagePort. For CPU-intensive background work, use utilityProcess with parentPort. 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 parentPort and MessagePort transfer
  • IPC input validation and channel allowlisting

Detailed Resources:


<philosophy>

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:

  1. Shared channel map + typed wrappers (DIY) -- define an IpcChannelMap interface, create thin typed wrappers around ipcMain/ipcRenderer. Zero dependencies, full control.
  2. 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.
  3. 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, and webContents.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
</philosophy>
<patterns>

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.

</patterns>

<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 ipcRenderer directly via contextBridge instead 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 window type with the preload API -- renderer code has no autocompletion
  • Using any for IPC payloads -- defeats the purpose of typed IPC

Architecture Issues:

  • Not cleaning up ipcRenderer.on listeners 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 of utilityProcess.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, or Set -- 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, not send or invoke -- the transfer list is a third argument
  • Main side must call port.start() explicitly -- forgetting this means no messages flow
  • port.close event fires when the remote end is garbage collected -- handle gracefully
  • SharedArrayBuffer is 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>

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

agents-inc のスキルをすべて見る

このスキルの問題を報告する