Install
$ agentstack add skill-recrsn-agent-skills-electron-to-electrobun ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Electron → Electrobun Migration
Two phases: compatibility check, then migration. For mechanical transformations (import rewrites, IPC channel renames, API mappings), prefer JS/TS codemods using jscodeshift or ts-morph over manual edits.
> Many Electrobun APIs are identical to Electron: BrowserWindow options (title, titleBarStyle, transparent), methods (setTitle, close, focus, minimize/isMinimized, maximize/unmaximize/isMaximized, setFullScreen/isFullScreen, setAlwaysOnTop/isAlwaysOnTop, setPosition, setSize), events (resize, focus), menu props (role, label, type, enabled, checked, submenu), GlobalShortcut, Screen, Session/Cookies. Tables below only list differences. Caveat: getPosition() → {x,y} not [x,y]; getSize() → {width,height} not [w,h].
Quick reference
| Electron | Electrobun | |---|---| | Node.js (V8) | Bun (JavaScriptCore) | | Bundled Chromium | System WebView (WebKit/WebView2/WebKitGTK) or CEF | | ipcMain.handle/ipcRenderer.invoke | BrowserView.defineRPC()/Electroview.defineRPC() | | webContents.send/ipcRenderer.on | RPC messages (fire-and-forget) | | preload + contextBridge | Typed RPC (preload supported, not for IPC bridging) | | dialog.showOpenDialog() | Utils.openFileDialog() | | dialog.showMessageBox() | Utils.showMessageBox() | | Menu.buildFromTemplate() | ApplicationMenu.setApplicationMenu([...]) | | Menu.popup() | ContextMenu.showContextMenu([...]) | | clipboard.* | Utils.clipboard*() | | new Notification() | Utils.showNotification({title,body,subtitle?,silent?}) | | app.getPath(name) | Utils.paths.* | | shell.openExternal/openPath/showItemInFolder | Utils.openExternal/openPath/showItemInFolder | | shell.trashItem | Utils.moveToTrash | | safeStorage.encrypt/decrypt | Bun.secrets.get/set/delete (OS keychain, key-value) | | app.quit() | Utils.quit() | | electron-builder/forge | electrobun build (built-in, BSDIFF patches) | | file:// / custom protocol | views://viewname/path | | CSS -webkit-app-region: drag | CSS class electrobun-webkit-app-region-drag |
RPC pattern
Key difference: Electron IPC is unidirectional — ipcMain.handle only serves renderer→main, webContents.send only pushes main→renderer. Electrobun RPC is bidirectional in a single schema — both sides can define requests (async call/response) and messages (fire-and-forget) in one type. This eliminates the need for separate IPC channel registrations, event forwarders, and bridge layers. A single defineRPC() call on each side replaces ipcMain.handle + webContents.send + contextBridge.exposeInMainWorld + ipcRenderer.invoke + ipcRenderer.on.
Shared schema type:
import type { RPCSchema } from "electrobun/bun";
type MySchema = {
bun: RPCSchema;
}>;
webview: RPCSchema;
messages: { myEvent: { data: string } };
}>;
};
bun.requests = renderer→main (returns response). bun.messages = renderer→main (fire-and-forget). webview.requests/messages = main→renderer.
Bun side:
const rpc = BrowserView.defineRPC({ handlers: { requests: {...}, messages: {...} } });
const win = new BrowserWindow({ url: "views://main/index.html", rpc });
win.webview.rpc.myEvent({ data: "hello" });
Webview side:
const rpc = Electroview.defineRPC({ handlers: { requests: {...}, messages: {...} } });
const view = new Electroview({ rpc });
await view.rpc.request.myMethod({ id: "123" });
Phase 1: Compatibility Check
1.1 Scan Electron API usage
Main: import {...} from 'electron' (list all), BrowserWindow (options/methods/events/webContents), ipcMain.handle/.on (channels), webContents.send (channels), dialog.*, Menu.*, Tray, clipboard.*, globalShortcut.*, screen.*, session/cookies, shell.*, Notification, app.getPath(), app.on('ready')/whenReady()/'window-all-closed'/'before-quit', safeStorage, nativeTheme, protocol.registerFileProtocol, autoUpdater/electron-updater
Preload: contextBridge.exposeInMainWorld (remove), ipcRenderer.* (remove). Categorize each file: IPC bridge (remove) vs non-IPC (keep).
Renderer: window.electron/window.api/custom bridge names, remote module
Build: electron-builder/forge config, Vite/webpack electron plugins
Native modules: *.node, node-gyp, prebuild, ffi-napi, better-sqlite3, keytar, node-pty
1.2 Unsupported patterns
remote → RPC requests. webRequest → limited. No equivalent: desktopCapturer, powerMonitor, powerSaveBlocker, TouchBar, crashReporter, vibrancy/visualEffectState (NSVisualEffectView), trafficLightPosition. systemPreferences → limited. nodeIntegration: true → N/A. dialog.showSaveDialog → none yet. @electron/rebuild → Bun native modules.
1.3 Report template
## Compatibility Report
### Direct equivalents
- [ ] BrowserWindow (N instances)
- [ ] IPC: N handles, M sends → RPC
- [ ] Dialogs, Menu, Clipboard, Shortcuts, Screen, Session
### Requires refactoring
- [ ] Preload IPC bridge (N files) → strip
- [ ] Preload non-IPC (N files) → keep
### No equivalent
- [ ] ...
### Effort: N schemas, N preloads, N handlers, N events
STOP. Present report. Enter plan mode and draft a migration plan based on the report. Get user approval before proceeding.
Phase 2: Migration
Type-check after each step.
2.1 Install
bun add electrobun
bun remove electron electron-builder @electron-forge/* electron-devtools-installer @electron/rebuild
Remove electron vite/webpack plugins.
2.2 electrobun.config.ts
import type { ElectrobunConfig } from "electrobun";
export default {
app: { name: "AppName", identifier: "com.x.app", version: "1.0.0" },
build: {
bun: { entrypoint: "src/main/index.ts" },
views: { main: { entrypoint: "src/renderer/main.tsx" } },
copy: { "src/renderer/index.html": "views/main/index.html" },
},
} satisfies ElectrobunConfig;
With Vite: keep for dev, use copy for prod output.
2.3 RPC schemas
Per window type, create schema in src/common/rpc/.
ipcMain.handle(ch) → bun.requests.ch. webContents.send(ch) → webview.messages.ch. ipcRenderer.invoke(ch) → rpc.request.ch(). ipcRenderer.on(ch) → message handler in defineRPC or rpc.addMessageListener.
Schema keys must be valid JS identifiers — rename :, ., / channels.
2.4 Preload
Strip: contextBridge.exposeInMainWorld(), all ipcRenderer, electron imports for these. Keep: polyfills, error handlers, globals, CSS injection, DOM prep. Delete file if only IPC bridge code. Wire kept preloads: preload: "views://main/preload.js". Remove global.d.ts/window.api type declarations.
2.5 Main process
BrowserWindow constructor:
| Electron | Electrobun | |---|---| | width,height,x,y | frame: {width,height,x,y} | | webPreferences.preload | preload (top-level, accepts views://, remote URL, inline JS) | | webPreferences.nodeIntegration | N/A (never available) | | webPreferences.contextIsolation | N/A (always isolated) | | webPreferences.partition | partition (persist: prefix for persistence) | | frame: false | styleMask: {Borderless:true, Titled:false} | | vibrancy / visualEffectState | No equivalent (use CSS backdrop-filter + transparent: true) | | trafficLightPosition | No equivalent (system default only) | | N/A | sandbox: true (disables RPC), html: "...", rpc |
styleMask (macOS): Borderless, Titled, Closable, Miniaturizable, Resizable, UnifiedTitleAndToolbar, FullScreen, FullSizeContentView, UtilityWindow, DocModalWindow, NonactivatingPanel, HUDWindow. titleBarStyle auto-sets styleMask.
BrowserWindow methods (differences only):
| Electron | Electrobun | |---|---| | restore() | unminimize() | | setBounds(rect) | setFrame(x,y,w,h) | | getBounds() | getFrame() → {x,y,width,height} | | loadURL(url) | webview.loadURL(url) | | webContents | webview (all webContents.* moves here) | | webContents.openDevTools({mode}) | webview.openDevTools() (no args) | | N/A | webview.toggleDevTools() |
BrowserWindow events (differences only):
| Electron | Electrobun | Data | |---|---|---| | 'closed' | 'close' | {id} | | 'move'/'moved' | 'move' | {id,x,y} |
Events also on Electrobun.events.on(name, ...).
App lifecycle:
// Electron:
app.whenReady().then(createWindow);
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
// Electrobun — no 'ready', starts immediately:
createWindow();
// exitOnLastWindowClosed in config. before-quit:
Electrobun.events.on('before-quit', (e) => { e.response = { allow: false }; });
openFileDialog options:
| Electron | Electrobun | |---|---| | properties: ['openFile'] | canChooseFiles: true | | properties: ['openDirectory'] | canChooseDirectory: true | | properties: ['multiSelections'] | allowsMultipleSelection: true | | defaultPath | startingFolder | | filters: [{extensions:[...]}] | allowedFileTypes: "png,jpg" (comma-sep, "*" for all) |
Returns string[] directly (not {filePaths, canceled}). showMessageBox options/return identical.
Menus:
// Electron: Menu.setApplicationMenu(Menu.buildFromTemplate(template));
// Electrobun — no buildFromTemplate:
ApplicationMenu.setApplicationMenu([
{ submenu: [{ label: 'Quit', role: 'quit' }] },
{ label: 'Edit', submenu: [{ role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }] },
]);
Menu item differences:
| Electron | Electrobun | |---|---| | click: () => {} | action: "string" + Electrobun.events.on('application-menu-clicked', e => e.data.action) | | accelerator: "CmdOrCtrl+S" | accelerator: "s" (just key, modifier auto-applied) | | visible: false | hidden: true | | toolTip | tooltip |
Roles: quit, hide, hideOthers, undo, redo, cut, copy, paste, pasteAndMatchStyle, delete, selectAll, minimize, close, toggleFullScreen, zoom, bringAllToFront, cycleThroughWindows. Separators: {type:"separator"} or {type:"divider"}. Linux: menus unsupported.
Context menus: ContextMenu.showContextMenu([...]) + Electrobun.events.on('context-menu-clicked', ...).
Shell: shell.* → Utils.*. trashItem → moveToTrash (no restore metadata on macOS). openExternal/openPath return boolean.
Clipboard: clipboard.*() → Utils.clipboard*(). readImage() → Uint8Array (PNG) or null. availableFormats() → ["text","image","files","html"].
Paths: app.getPath(name) → Utils.paths.{name}. All sync. userData is app-scoped: {appData}/{identifier}/{channel}. Extra: Utils.paths.config, .cache, .userCache, .userLogs.
Credentials:
// Electron: safeStorage.encryptString(value); safeStorage.decryptString(buffer);
// Electrobun (OS keychain, key-value):
await Bun.secrets.set({ service: "my-app", name: "api-key", value: "secret" });
await Bun.secrets.get({ service: "my-app", name: "api-key" }); // string | null
await Bun.secrets.delete({ service: "my-app", name: "api-key" }); // boolean
Not raw encrypt/decrypt — refactor to key-value by service+name.
GlobalShortcut, Screen, Session/Cookies: Import from "electrobun/bun". Same APIs.
2.6 Renderer
Replace bridge calls: window.api.call(ch, args) → rpc.request.ch(args). window.api.on(ch, cb) → rpc.addMessageListener('ch', cb).
Per-entrypoint rpc.ts:
import { Electroview } from "electrobun/view";
import type { MySchema } from "../common/rpc/my-schema.js";
export const rpc = Electroview.defineRPC({ handlers: { requests: {}, messages: {} } });
const view = new Electroview({ rpc });
CSS: -webkit-app-region: drag/no-drag → classes electrobun-webkit-app-region-drag/-no-drag.
Remove window.api/window.electron type declarations, global.d.ts/preload.d.ts.
2.7 Build
With Vite: keep for dev, copy maps output in electrobun.config.ts. Without: build.views in config.
2.8 Verify
bun run typecheck → electrobun build → test windows/RPC/events/menus/dialogs → electrobun build --env=stable
Pitfalls
- Preload ≠ delete — strip only
contextBridge/ipcRenderer; keep polyfills/globals - Channel names — RPC keys must be valid JS identifiers, rename
:./consistently safeStorage→Bun.secrets— key-value, not encrypt/decryptremote→ must become RPC requests- Native modules — may need Bun-compatible alternatives
- Multi-window — each window type needs its own RPC schema
- Draggable regions — CSS property → CSS class
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: recrsn
- Source: recrsn/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.