
Electron Skills
- 7 installs
- 1 repo stars
- Updated July 23, 2026
- mym0404/agent-skills
Helps with ai & agent building tasks.
About
electron-skills is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
- electron-skills
- AI & Agent Building
- AI-coding skill
Electron Skills by the numbers
- 7 all-time installs (skills.sh)
- Ranked #12,545 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mym0404/agent-skills --skill electron-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 23, 2026 |
| Repository | mym0404/agent-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Electron Skills
End-to-end type-safe IPC architecture for Electron apps.
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | IPC Architecture | CRITICAL | ipc- |
Quick Reference
ipc-type-safe-architecture— Central API type, helper functions, preload bridges, main handlers, 5-step wiring processipc-app-error-system— Structured IPC error handling with internal parseAppError and renderer-facing catchAppError
How to Use
Read rule files for examples:
rules/ipc-type-safe-architecture.md
rules/ipc-app-error-system.md{
"version": "1.0.1",
"organization": "Engineering",
"date": "March 2026",
"abstract": "Type-safe Electron IPC architecture with centralized API types, helper functions, and a structured AppError flow where renderer code uses catchAppError.",
"references": [
"https://www.electronjs.org/docs/latest/tutorial/ipc",
"https://www.electronjs.org/docs/latest/api/context-bridge",
"https://www.electronjs.org/docs/latest/api/ipc-main",
"https://www.electronjs.org/docs/latest/tutorial/process-model"
]
}
Sections
1. IPC Architecture (ipc)
Impact: CRITICAL Description: End-to-end type-safe IPC communication between Electron main, preload, and renderer processes using centralized API types and helper functions.
Structured Error Handling Across IPC Boundary
Electron IPC serializes errors as strings. A structured error system ensures main process errors are thrown with a code + message + payload, serialized across IPC, parsed internally, and routed in the renderer through a single public entrypoint: catchAppError.
Incorrect — unstructured error handling:
// main process
ipcMain.handle("save-user", async (_e, user) => {
if (!user.name) throw new Error("Name is required");
// ...
});
// renderer
try {
await window.api.user.saveUser({ user });
} catch (e) {
// e.message is mangled by IPC serialization
// No way to distinguish error types
alert(e.message);
}Error messages get mangled crossing the IPC boundary. No structured way to route different errors to different handlers. Renderer can't distinguish "validation failed" from "database error."
Correct — AppError system:
1. Define the error system
// src/domain/types/AppError.ts
// Error class for IPC serialization
export class AppErrorClass extends Error {}
// All error codes as a union type
export type AppErrorCode =
| "user_not_found"
| "validation_failed"
| "duplicate_entry"
| "file_not_found"
| "unknown";
// Human-readable messages per code
export const appErrorMessages: Record<AppErrorCode, string> = {
user_not_found: "User not found.",
validation_failed: "Data validation failed.",
duplicate_entry: "A record with this identifier already exists.",
file_not_found: "The specified file could not be found.",
unknown: "An unknown error occurred.",
};
export type AppError = {
message: string;
code: AppErrorCode;
payload: any;
};2. Throw errors in main process
throwAppError serializes the error as JSON inside an AppErrorClass instance so it survives IPC serialization.
// Short form — code only (message auto-resolved from appErrorMessages)
throwAppError("user_not_found");
// Full form — custom message and/or payload
throwAppError({
code: "validation_failed",
message: "Field 'email' is invalid",
payload: { field: "email", value: input.email },
});Implementation:
export function throwAppError(code: AppErrorCode): never;
export function throwAppError(options: {
message?: string;
code?: AppErrorCode;
payload?: object;
}): never;
export function throwAppError(
codeOrOptions:
| AppErrorCode
| { message?: string; code?: AppErrorCode; payload?: object },
): never {
if (typeof codeOrOptions === "string") {
throw new AppErrorClass(
JSON.stringify({
message: appErrorMessages[codeOrOptions],
code: codeOrOptions,
}),
);
}
const {
code = "unknown",
message = appErrorMessages[code],
payload,
} = codeOrOptions;
throw new AppErrorClass(JSON.stringify({ message, code, payload }));
}3. Parse errors across IPC boundary internally
parseAppError extracts the JSON-encoded AppError from an Error.message that may be wrapped by Electron's IPC layer. Treat it as an internal helper for IPC utilities such as catchAppError and registerIpcMainHandle, not as a renderer-facing API.
export const parseAppError = (error: unknown): AppError | null => {
if (error instanceof Error) {
// Extract JSON object from the end of the message
// (Electron IPC may prepend extra text)
try {
let braceCount = 0;
let startIndex = -1;
const message = error.message.trim();
for (let i = message.length - 1; i >= 0; i--) {
if (message[i] === "}") braceCount++;
else if (message[i] === "{") {
braceCount--;
if (braceCount === 0) { startIndex = i; break; }
}
}
if (startIndex !== -1) {
const parsed = JSON.parse(message.substring(startIndex));
if (parsed && typeof parsed.code === "string") return parsed;
}
} catch {
return null;
}
}
return null;
};4. Catch errors in renderer by code
catchAppError is the renderer-facing error utility. It parses the error internally and routes to the matching handler by code. Unmatched codes fall through to DEFAULT.
export const catchAppError = (
e: unknown,
handlers: Partial<
Record<AppErrorCode | "DEFAULT", (error: AppError) => void>
>,
): void => {
const appError = parseAppError(e);
if (appError) {
const handler = handlers[appError.code] || handlers["DEFAULT"];
handler?.(appError);
return;
}
handlers["DEFAULT"]?.({
message: appErrorMessages["unknown"],
code: "unknown",
payload: e,
});
};Renderer usage:
// React Query onError
onError: (e) => {
catchAppError(e, {
validation_failed: ({ payload }) => {
setErrorField(payload.field);
showErrorToast("Validation failed");
},
duplicate_entry: () => {
showErrorToast("This record already exists");
},
DEFAULT: ({ code, message }) => {
showErrorToast(`${code}: ${message}`);
},
});
},Renderer rule:
- Use
catchAppErrordirectly insidecatch,onError, or similar renderer error boundaries. - Do not call
parseAppErrordirectly from renderer code. - Do not add extra renderer-side app-error utilities unless there is a concrete need that
catchAppErrorcannot handle.
5. Wire into registerIpcMainHandle
The IPC handler helper catches errors thrown in main process handlers, preserves AppErrorClass instances, and re-wraps unstructured errors.
export const registerIpcMainHandle = <T extends keyof API, K extends keyof API[T]>(
channel: string,
handler: APIFunction<T, K>,
): void => {
ipcMain.handle(channel, async (_event, ...args: unknown[]) => {
try {
return await (handler as any)(...args);
} catch (error) {
if (error instanceof AppErrorClass) throw error;
const appError = parseAppError(error);
if (appError) throw new AppErrorClass(JSON.stringify(appError));
throw error;
}
});
};Key rules
- Every known error condition gets an
AppErrorCodeentry - Main process uses
throwAppError— never rawthrow new Error - Renderer uses
catchAppErrorwith explicit code handlers +DEFAULTfallback parseAppErroris internal-only and handles Electron's IPC message wrapping- Renderer should not need
parseAppErroror additional app-error helper utilities beyondcatchAppError registerIpcMainHandlepreservesAppErrorClassacross IPC boundary- Add new error codes to the
AppErrorCodeunion andappErrorMessagesrecord together
Reference: Electron IPC Error Handling
Type-Safe IPC Architecture
End-to-end type-safe Electron IPC using a central API type, helper functions (createIpcInvoker, registerIpcMainHandle), and a consistent 5-step wiring process.
Incorrect — scattered, untyped IPC:
// preload.ts — no type link, any params
contextBridge.exposeInMainWorld("api", {
getUser: (id: string) => ipcRenderer.invoke("get-user", id),
saveUser: (user: any) => ipcRenderer.invoke("save-user", user),
});
// main.ts — channel strings can silently drift
ipcMain.handle("get-user", (_e, id) => db.findUser(id));Channel names are strings with no compile-time link. Parameter/return types are any. Renaming a channel in one place silently breaks the other.
Correct — centralized type-safe architecture:
1. Central API type (single source of truth)
// src/domain/api/API.ts
type UserAPI = {
getUser: (params: { id: string }) => Promise<User>;
saveUser: (params: { user: Omit<User, "id" | "createdAt"> }) => Promise<void>;
deleteUser: (params: { id: string }) => Promise<void>;
};
type DialogAPI = {
openFile: (params: { extensions: string[] }) => Promise<string | null>;
};
export type API = {
user: UserAPI;
dialog: DialogAPI;
};
// Extract function type for a specific API method
export type APIFunction<T extends keyof API, K extends keyof API[T]> = API[T][K];
// Extract parameter type for a specific API method
export type APIParams<T extends keyof API, K extends keyof API[T]> = Parameters<API[T][K]>[0];Rules: one sub-type per domain, every method takes a single object param, every method returns Promise<T>.
2. Helper functions
// src/domain/api/API.ts (same file)
// Preload side — creates a typed invoker for a channel
export const createIpcInvoker = <T extends keyof API, K extends keyof API[T]>(
channel: string,
): APIFunction<T, K> => {
return ((...args: unknown[]) =>
ipcRenderer.invoke(channel, ...args)) as APIFunction<T, K>;
};
// Main side — registers a typed handler for a channel
export const registerIpcMainHandle = <T extends keyof API, K extends keyof API[T]>(
channel: string,
handler: APIFunction<T, K>,
): void => {
ipcMain.handle(channel, async (_event, ...args: unknown[]) => {
return await (handler as any)(...args);
});
};3. Preload bridge
One bridge file per domain. Uses createIpcInvoker — never raw ipcRenderer.
// src/preload/bridges/userBridge.ts
import { createIpcInvoker } from "@domain/api/API";
export const userBridge = {
getUser: createIpcInvoker<"user", "getUser">("app:user-get"),
saveUser: createIpcInvoker<"user", "saveUser">("app:user-save"),
deleteUser: createIpcInvoker<"user", "deleteUser">("app:user-delete"),
};Bridges assembled and exposed in preload entry:
// src/preload/index.ts
import { contextBridge } from "electron";
import { userBridge } from "./bridges/userBridge";
import { dialogBridge } from "./bridges/dialogBridge";
const api = {
user: userBridge,
dialog: dialogBridge,
};
contextBridge.exposeInMainWorld("api", api);Window type declaration:
// src/preload/index.d.ts
import type { API } from "@domain/api/API";
declare global {
interface Window {
api: API;
}
}4. Main process handlers
Business logic in index.ts typed with APIFunction. Registration in ipcHandlers.ts using registerIpcMainHandle.
// src/main/feature/user/index.ts
import type { APIFunction } from "@domain/api/API";
export const getUser: APIFunction<"user", "getUser"> = async ({ id }) => {
return db.findUser(id);
};
export const saveUser: APIFunction<"user", "saveUser"> = async ({ user }) => {
return db.create(user);
};// src/main/feature/user/ipcHandlers.ts
import { registerIpcMainHandle } from "@domain/api/API";
import { getUser, saveUser, deleteUser } from "./index";
export const registerUserIpcHandlers = () => {
registerIpcMainHandle<"user", "getUser">("app:user-get", getUser);
registerIpcMainHandle<"user", "saveUser">("app:user-save", saveUser);
registerIpcMainHandle<"user", "deleteUser">("app:user-delete", deleteUser);
};All feature registrations called from main entry:
// src/main/index.ts
import { registerUserIpcHandlers } from "./feature/user/ipcHandlers";
import { registerDialogIpcHandlers } from "./feature/dialog/ipcHandlers";
const registerIpcHandlers = () => {
registerUserIpcHandlers();
registerDialogIpcHandlers();
};
registerIpcHandlers();5. Renderer usage
After wiring, the renderer calls APIs with full type safety:
const user = await window.api.user.getUser({ id: "abc123" });
await window.api.user.saveUser({
user: { name: "Kim", email: "kim@example.com" },
});Adding a new IPC API — 5-step checklist
| Step | File | Action |
|---|---|---|
| 1 | src/domain/api/API.ts | Add domain type to API |
| 2 | src/main/feature/{domain}/index.ts | Implement with APIFunction typing |
| 3 | src/main/feature/{domain}/ipcHandlers.ts | Register with registerIpcMainHandle |
| 4 | src/preload/bridges/{domain}Bridge.ts | Create with createIpcInvoker |
| 5 | src/preload/index.ts | Add bridge to api object |
Channel naming convention
{app-prefix}:{domain}-{action}Examples: app:user-get, app:store-set, app:dialog-open-file
Common mistakes
- Mismatched channel strings — bridge says
"app:notify-send"but handler registers"app:notification-send". No compile error, silent failure at runtime. - Forgetting to register — handler function exists but
registerIpcMainHandleis never called. - Missing bridge in preload entry — bridge file exists but not imported in
index.ts. Renderer getsundefined.
Reference: