Electron logo

Electron

Community
ericrisco
electron

Use when building, hardening, or shipping a cross-platform Electron desktop app — main/renderer/preload process model, typed contextBridge IPC, locking down nodeIntegration/contextIsolation/sandbox/CSP, or packaging with signing and auto-update. NOT a Rust-backed shell on the native webview (that is tauri), nor the web UI inside it (that is react).

Overview

Publisherericrisco
Repositoryrsc-harness
Skill nameelectron
Stars
239
Forks
24
Bundled files
4
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 4 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by ericrisco on GitHub. Read the source before you install it.

Installation

Install the Electron AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/ericrisco/rsc-harness/tree/main/skills/electron
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/ericrisco/rsc-harness.git /tmp/rsc-harness
mkdir -p .claude/skills
cp -r /tmp/rsc-harness/skills/electron .claude/skills/electron
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Electron in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Electron on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Electron is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Electron — desktop shell, typed IPC, hardening, signing

This skill owns the desktop shell: process model, IPC, security, packaging, signing, auto-update. It does not own the web UI inside the window (../react/SKILL.md), the Node backend logic, or the CI runner matrix.

The mental model — three processes, one rule

An Electron app is three kinds of process. Code lives in exactly one; putting it in the wrong one is the root cause of most security holes.

ProcessRuntimeTrustOne perDoes
mainNode.js, full OS APItrustedappwindows, menus, tray, dialogs, fs, child procs
rendererChromium, no Nodeuntrustedwindowyour web UI; can run attacker JS if you load remote content
preloadisolated world, runs before page JSsemi-trustedwindowthe only bridge: contextBridge exposes a tiny API

The governing rule: the renderer is untrusted, the main process holds all privilege, and the preload is the only sanctioned bridge between them. Renderer-to-OS escalation is the dominant failure mode in real Electron apps, so everything below is a corollary.

Start right

Scaffold with Electron Forge (@electron/forge) — first-party, all-in-one (scaffold → package → make → publish), and it gets new Electron features first.

bash
npm init electron-app@latest my-app -- --template=vite-typescript
cd my-app && npm start

Pin to a supported major. Electron ships a new major every 8 weeks (tracking Chromium) and supports only the latest 3 majors. As of June 2026 the stable line is Electron 42 (Chromium M148, Node 24); 43 lands 2026-06-30. Shipping on an EOL major means unpatched Chromium CVEs — check package.json and bump if behind.

Project layout keeps the boundary visible:

text
src/
  main.ts       # main process — owns everything privileged
  preload.ts    # the bridge — contextBridge only
  renderer/     # your web UI (untrusted)
  ipc/types.ts  # IPC contract shared by main + preload

The security baseline

Modern Electron defaults are already secure (nodeIntegration:false, contextIsolation:true, sandbox:true since Electron 20). Assert them explicitly anyway so a careless edit can't silently weaken the window:

ts
const win = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    nodeIntegration: false,        // renderer gets NO require/process — never flip true
    contextIsolation: true,        // preload + page run in separate JS worlds
    sandbox: true,                 // renderer in an OS sandbox; preload uses a limited API
    webSecurity: true,             // keep same-origin policy; never disable to "fix CORS"
    allowRunningInsecureContent: false, // no mixed http content on https pages
  },
});

One why per flag: each removes a documented way for renderer-side script to reach Node or the OS. Flipping any of them to the insecure value is what verify.sh fails on.

CSP via response headers, not a <meta> tag — meta CSP can't restrict the initial document and is trivially bypassed for some directives. Set it in the main process:

ts
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
  cb({ responseHeaders: { ...details.responseHeaders,
    'Content-Security-Policy': ["default-src 'self'; script-src 'self'"] } });
});

Lock navigation. A renderer that can navigate to attacker content gets the renderer's privileges. Deny unexpected navigation and block new windows:

ts
app.on('web-contents-created', (_e, contents) => {
  contents.on('will-navigate', (e, url) => {
    if (new URL(url).origin !== 'https://app.local') e.preventDefault();
  });
  contents.setWindowOpenHandler(() => ({ action: 'deny' })); // no tab-jacking
});

Open real external links deliberately, after allow-listing the protocol:

ts
function openExternal(url: string) {
  const { protocol } = new URL(url);
  if (protocol === 'https:' || protocol === 'mailto:') shell.openExternal(url);
}

Which branch are you on?

  • Local-only UI (you bundle the HTML/JS): CSP + sandbox:true + nav lockdown is enough.
  • Loads any remote/third-party content: also add Electron Fuses (disable run-as-node, encrypt cookies, ASAR integrity) and treat every embedded origin as hostile.

Fuses and the full hardened example live in references/security-and-ipc.md.

Typed IPC the right way

IPC is the seam between untrusted renderer and privileged main. Get it wrong and you've handed the OS to whatever script runs in the page.

Never expose ipcRenderer (or any of its methods) across the bridge. Sending the whole module now yields an empty object on the other side — a deliberate footgun removal — and exposing its methods lets the page call any channel with any payload.

ts
// Bad — preload.ts: hands the renderer a universal IPC weapon (also: empty object now)
contextBridge.exposeInMainWorld('api', ipcRenderer);
ts
// Good — preload.ts: ONE function per channel, each wrapping a specific call
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('api', {
  readConfig: () => ipcRenderer.invoke('config:read'),
  saveNote: (text: string) => ipcRenderer.invoke('note:save', text),
  onSync: (cb: () => void) => ipcRenderer.on('sync:done', cb), // events: send/on
});

Prefer ipcMain.handle + ipcRenderer.invoke (request/response, returns a Promise) for anything that returns data. Reserve send/on for fire-and-forget events (progress, push notifications). Validate every argument in main — a renderer message is an HTTP request from an untrusted client:

ts
ipcMain.handle('note:save', (_e, text: unknown) => {
  if (typeof text !== 'string' || text.length > 10_000) throw new Error('bad input');
  return saveNote(text); // never path.join(userInput) or eval it
});

Share the contract as TypeScript types across both sides (ipc/types.ts) so a channel rename breaks the build, not production. Full main + preload + window.api d.ts example: references/security-and-ipc.md.

Native capabilities — renderer asks, main acts

The renderer can't (and must not) touch the OS directly. When the UI needs a native menu, tray icon, file dialog, system notification, custom protocol:// handler, or a child_process, the renderer invokes an IPC channel and the main process performs the action and returns a result. Same one-function-per-channel discipline as above.

For embedding web content in a region of a window, use WebContentsView — BrowserView is deprecated since Electron 30. They share shape (both take webPreferences; setBounds/getBounds/webContents carry over), so migration is mechanical.

Packaging, signing, auto-update

Two real toolchains:

NeedUse
New app, first-party alignment, features firstElectron Forge (ASAR integrity, universal macOS, scaffold→make→publish)
Differential/staged updates, multi-provider (GitHub/S3), richer configelectron-builder + electron-updater

Code signing is a prerequisite for auto-update, not optional polish. macOS auto-update (Squirrel.Mac) refuses to update an app that isn't signed and notarized; Windows updates need an Authenticode-signed installer. So the order is always: sign → notarize → publish → auto-update. Full Forge and builder configs, notarytool steps, Windows Authenticode, and electron-updater + GitHub Releases wiring: references/packaging-and-updates.md.

The CI matrix that runs these builds across three OSes is github-actions' job; this skill defines what to build and sign.

Anti-patterns and migration smells

Anti-patternWhy it's wrongDo instead
nodeIntegration: truePage JS gets require('fs'); any XSS becomes OS-level RCEfalse; move the capability behind IPC
contextIsolation: falsePage can rewrite the preload's globalstrue (the default)
sandbox: false without a reasonDrops the OS sandbox around the renderertrue; only relax for a measured, isolated need
exposeInMainWorld('api', ipcRenderer) or its methodsUniversal IPC weapon — any channel, any payload (and an empty object now)One typed function per channel
No arg validation in ipcMain.handleRenderer is an untrusted client; you trust its inputType-check + bound every arg before acting
webSecurity: false to "fix CORS"Disables same-origin policy app-wideKeep true; proxy/handle CORS in main
CSP only in a <meta> tagDoesn't cover the initial document; bypassableSet CSP in onHeadersReceived
Loading a remote URL into a Node-enabled windowRemote site runs with your app's privilegeBundle UI locally; sandbox + nav lockdown for remote
@electron/remote importSync main-object access = renderer→main RCEReplace with explicit ipcMain.handle channels
new BrowserView(...)Deprecated since Electron 30, will be removednew WebContentsView(...)
Auto-update with an unsigned/un-notarized buildSquirrel.Mac silently refuses; no updates shipSign + notarize (mac), Authenticode (win) first
Shipping on an EOL Electron majorUnpatched Chromium CVEs in your users' handsStay within the latest 3 majors
Heavy CPU work in the main processBlocks the event loop → the whole UI freezesutilityProcess/worker, or do it in the renderer

Verify

Run scripts/verify.sh /path/to/your-electron-project to grep a target for insecure patterns (nodeIntegration: true, contextIsolation: false, sandbox: false, @electron/remote, new BrowserView, exposeInMainWorld(..., ipcRenderer)). It's read-only and exits non-zero on any finding. With no argument it self-checks this skill's own example snippets for the secure baseline. See references/security-and-ipc.md for the full checklist.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Electron AI skill do?

Use when building, hardening, or shipping a cross-platform Electron desktop app — main/renderer/preload process model, typed contextBridge IPC, locking down nodeIntegration/contextIsolation/sandbox/CSP, or packaging with signing and auto-update. NOT a Rust-backed shell on the native webview (that is tauri), nor the web UI inside it (that is react).

Why use Electron on TypingMind?

Because you install it once and use it with any model. Electron is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Electron in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/ericrisco/rsc-harness/tree/main/skills/electron. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Electron?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Electron?

As many as you like. As long as a model supports skills, you can use Electron with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Electron AI skill free?

Yes. It is published on GitHub by ericrisco under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇