Source profileQuality 92/100

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…

Best for

    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

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    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.

    Source-detected install commandSource
    npx skills add https://github.com/agents-inc/skills --skill "src/skills/desktop-ipc-electron"
    Safe inspection promptEditorial

    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

    1. 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.
    2. 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
    3. 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.
    4. 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.
    5. 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

    low · line 194

    The documentation asks the agent to read local files, directories, or repositories.

    readFile: t.procedure

    Reads files

    low · line 197

    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

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score92/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars23SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated 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, 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

    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

    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 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>

    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.