
Electrobun Best Practices
- 280 installs
- 52 repo stars
- Updated June 24, 2026
- 0xbigboss/claude-code
electrobun-best-practices is a Claude Code agent skill that scaffolds and ships cross-platform desktop applications with Electrobun, Bun bundling, TypeScript layout, native APIs, and secure renderer patterns for develope
About
electrobun-best-practices is a 0xbigboss/claude-code skill guiding agents through Electrobun desktop development with Bun as the main-process runtime and TypeScript across main and renderer layers. Electrobun targets macOS, Windows, and Linux using system webviews—WKWebView, WebView2, and WebKitGTK—instead of bundling Chromium, producing lean binaries often cited around 14MB versus Electron-scale bundles. The skill covers electrobun.config.ts setup, typed RPC between Bun main and webview renderer, BrowserWindow lifecycle, optional CEF bundling on Linux via bundleCEF, and GitHub Actions matrix builds per platform. Reach for electrobun-best-practices when scaffolding a new Electrobun app, hardening renderer security, or preparing cross-platform release artifacts. It suits TypeScript-first developers who want desktop distribution without Rust or a bundled browser engine, accepting per-platform webview rendering differences.
- Electrobun project scaffolding
- Bun-native desktop bundling
- Cross-platform window management
- Native OS API integration
- Renderer-main security separation
Electrobun Best Practices by the numbers
- 280 all-time installs (skills.sh)
- Ranked #764 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/0xbigboss/claude-code --skill electrobun-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 280 |
|---|---|
| repo stars | ★ 52 |
| Last updated | June 24, 2026 |
| Repository | 0xbigboss/claude-code ↗ |
How do you build cross-platform desktop apps with Electrobun?
Scaffold and ship cross-platform desktop apps with Electrobun using Bun bundling, TypeScript layout, native APIs, and secure renderer patterns.
Who is it for?
TypeScript developers shipping small cross-platform desktop apps who want Bun bundling and system webviews instead of Electron's Chromium bundle.
Skip if: Teams needing guaranteed cross-browser rendering parity, a mature plugin ecosystem, or deep native UI without webview layers.
When should I use this skill?
The user asks to scaffold, configure, or ship an Electrobun desktop application with Bun and TypeScript.
What you get
An Electrobun project layout, electrobun.config.ts, typed RPC wiring, and platform build artifacts for macOS, Windows, and Linux.
- Electrobun project scaffold
- electrobun.config.ts
- cross-platform build artifacts
By the numbers
- Targets 3 desktop platforms: macOS, Windows, and Linux
- Electrobun apps commonly land around 14MB using system webviews
Files
Electrobun Best Practices
Electrobun builds cross-platform desktop apps with TypeScript and Bun. This skill gives safe defaults, typed RPC patterns, and operational guidance for build/update/distribution.
Docs: https://blackboard.sh/electrobun/docs/
Pair with TypeScript Best Practices
Always load typescript-best-practices alongside this skill.
Version and Freshness
Electrobun APIs evolve quickly. Before relying on advanced options or platform-specific behavior, verify against current docs and CLI output.
Architecture
Electrobun apps run as Bun apps:
- Bun process (main): imports from
electrobun/bun - Browser context (views): imports from
electrobun/view - Shared types: RPC schemas shared between both contexts
IPC between bun and browser contexts uses postMessage, FFI, and (in some paths) encrypted WebSockets.
Quick Start
bunx electrobun init
bun install
bun startRecommended scripts:
{
"scripts": {
"start": "electrobun run",
"dev": "electrobun dev",
"dev:watch": "electrobun dev --watch",
"build:dev": "bun install && electrobun build",
"build:canary": "electrobun build --env=canary",
"build:stable": "electrobun build --env=stable"
}
}Secure Defaults
Use this baseline for untrusted or third-party content:
import { BrowserWindow } from "electrobun/bun";
const win = new BrowserWindow({
title: "External Content",
url: "https://example.com",
sandbox: true, // disables RPC, events still work
partition: "persist:external",
});
win.webview.setNavigationRules([
"^*", // block everything by default
"*://example.com/*", // allow only trusted domain(s)
"^http://*", // enforce HTTPS
]);
win.webview.on("will-navigate", (e) => {
console.log("nav", e.data.url, "allowed", e.data.allowed);
});Security checklist:
- Use
sandbox: truefor untrusted content. - Apply strict navigation allowlists.
- Use separate
partitionvalues for isolation. - Validate all
host-messagepayloads from<electrobun-webview>preload scripts. - Do not write to
PATHS.RESOURCES_FOLDERat runtime; useUtils.paths.userData.
Typed RPC (Minimal Pattern)
// src/shared/types.ts
import type { RPCSchema } from "electrobun/bun";
export type MyRPC = {
bun: RPCSchema<{
requests: {
getUser: { params: { id: string }; response: { name: string } };
};
messages: {
logToBun: { msg: string };
};
}>;
webview: RPCSchema<{
requests: {
updateUI: { params: { html: string }; response: boolean };
};
messages: {
notify: { text: string };
};
}>;
};// bun side
import { BrowserView, BrowserWindow } from "electrobun/bun";
import type { MyRPC } from "../shared/types";
const rpc = BrowserView.defineRPC<MyRPC>({
handlers: {
requests: {
getUser: ({ id }) => ({ name: `user-${id}` }),
},
messages: {
logToBun: ({ msg }) => console.log(msg),
},
},
});
const win = new BrowserWindow({
title: "App",
url: "views://mainview/index.html",
rpc,
});
await win.webview.rpc.updateUI({ html: "<p>Hello</p>" });// browser side
import { Electroview } from "electrobun/view";
import type { MyRPC } from "../shared/types";
const rpc = Electroview.defineRPC<MyRPC>({
handlers: {
requests: {
updateUI: ({ html }) => {
document.body.innerHTML = html;
return true;
},
},
messages: {
notify: ({ text }) => console.log(text),
},
},
});
const electroview = new Electroview({ rpc });
await electroview.rpc.request.getUser({ id: "1" });
electroview.rpc.send.logToBun({ msg: "hello" });Events and Shutdown
Use before-quit for shutdown cleanup instead of relying on process.on("exit") for async work.
import Electrobun from "electrobun/bun";
Electrobun.events.on("before-quit", async (e) => {
await saveState();
// e.response = { allow: false }; // optional: cancel quit
});Important caveat:
- Linux currently has a caveat where some system-initiated quit paths (for example Ctrl+C/window-manager/taskbar quit) may not fire
before-quit. Programmatic quit viaUtils.quit()/process.exit()is reliable.
Common Patterns
- Keyboard shortcuts (copy/paste/undo): define an Edit
ApplicationMenuwith role-based items. - Tray-only app: set
runtime.exitOnLastWindowClosed: false, then drive UX fromTray. - Multi-account isolation: use separate
partitionvalues per account. - Chromium consistency: set
bundleCEF: trueanddefaultRenderer: "cef"in platform config.
Troubleshooting
- RPC calls fail unexpectedly:
- Check whether the target webview is sandboxed (
sandbox: truedisables RPC). - Confirm shared RPC types match both bun and browser handlers.
- Navigation blocks legitimate URLs:
- Review
setNavigationRulesordering; last match wins. - Keep
^*first only when you intentionally run strict allowlist mode. - Updater says no update:
- Verify
release.baseUrland uploadedartifacts/naming ({channel}-{os}-{arch}-...). - Confirm channel/build env alignment (
canaryvsstable). - User sessions leak across accounts:
- Use explicit per-account partitions and manage cookies via
Session.fromPartition(...). - Build hooks not running:
- Ensure hook paths are correct and executable via Bun.
- Inspect hook env vars (for example
ELECTROBUN_BUILD_ENV,ELECTROBUN_OS,ELECTROBUN_ARCH).
Reference Files
- Build config, artifacts, and hooks: reference/build-config.md
- BrowserWindow, BrowserView, and webview tag APIs: reference/window-and-webview.md
- Menus, tray, events, updater, utils/session APIs: reference/platform-apis.md
Build Config Reference
Contents
- electrobun.config.ts baseline
- Build fields and runtime fields
views://bundled assets- Distribution artifact model
- Build lifecycle hooks
electrobun.config.ts Baseline
import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "My App",
identifier: "com.example.myapp",
version: "1.0.0",
urlSchemes: ["myapp"], // macOS deep-linking support
},
runtime: {
exitOnLastWindowClosed: true,
// custom keys are readable at runtime via BuildConfig.get()
},
build: {
bun: {
entrypoint: "src/bun/index.ts",
// Bun.build pass-through options supported (plugins/external/sourcemap/minify/etc.)
},
views: {
mainview: {
entrypoint: "src/mainview/index.ts",
},
},
copy: {
"src/mainview/index.html": "views/mainview/index.html",
"src/mainview/style.css": "views/mainview/style.css",
},
useAsar: false,
asarUnpack: ["*.node", "*.dll", "*.dylib", "*.so"],
// watch: ["scripts"],
// watchIgnore: ["**/*.generated.*"],
mac: {
codesign: true,
notarize: true,
bundleCEF: false,
defaultRenderer: "native", // or "cef" when bundleCEF is true
entitlements: {},
icons: "icon.iconset",
},
},
scripts: {
preBuild: "./scripts/pre-build.ts",
postBuild: "./scripts/post-build.ts",
postWrap: "./scripts/post-wrap.ts",
postPackage: "./scripts/post-package.ts",
},
release: {
baseUrl: "https://storage.example.com/myapp/",
},
} satisfies ElectrobunConfig;Runtime Access
import { BuildConfig } from "electrobun/bun";
const cfg = await BuildConfig.get();
console.log(cfg.runtime?.exitOnLastWindowClosed);Bundled Assets (views://)
views:// maps to bundled static assets and works in BrowserWindow/BrowserView URLs plus HTML and CSS references.
<script src="views://mainview/index.js"></script>
<link rel="stylesheet" href="views://mainview/style.css" />
<img src="views://assets/logo.png" />Distribution Artifacts
Non-dev builds (canary/stable) produce flat artifacts prefixed:
{channel}-{os}-{arch}-update.json- platform installers
.tar.zstupdate bundle.patchincremental patch (typically from previous version)
General guidance:
- Upload entire
artifacts/output to static hosting. - Keep historical patch files if you want chain-style incremental updates available to clients.
- If patch trail is unavailable, updater falls back to full bundle download.
Build Lifecycle Hooks
Execution order:
preBuildpostBuildpostWrappostPackage
Common env vars:
ELECTROBUN_BUILD_ENVELECTROBUN_OSELECTROBUN_ARCHELECTROBUN_BUILD_DIRELECTROBUN_APP_NAMEELECTROBUN_APP_VERSIONELECTROBUN_APP_IDENTIFIERELECTROBUN_ARTIFACT_DIRELECTROBUN_WRAPPER_BUNDLE_PATH(postWrap only)
Platform APIs
Contents
- Application and context menus
- System tray
- Global events and shutdown lifecycle
- Updater
- Utils
- GlobalShortcut, Screen, Session
Application Menu
import Electrobun, { ApplicationMenu } from "electrobun/bun";
ApplicationMenu.setApplicationMenu([
{ submenu: [{ label: "Quit", role: "quit" }] },
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{ label: "Save", action: "save", accelerator: "s" },
],
},
]);
Electrobun.events.on("application-menu-clicked", (e) => {
console.log("action", e.data.action);
});Use role-based items to enable native shortcuts (quit, copy, paste, etc.).
Context Menu
import Electrobun, { ContextMenu } from "electrobun/bun";
ContextMenu.showContextMenu([
{ label: "Copy", role: "copy" },
{ label: "Paste", role: "paste" },
{ type: "separator" },
{ label: "Custom", action: "custom", accelerator: "s" },
]);
Electrobun.events.on("context-menu-clicked", (e) => {
console.log("context action", e.data.action);
});System Tray
import { Tray } from "electrobun/bun";
const tray = new Tray({
title: "My App",
image: "views://assets/icon-32-template.png",
template: true,
width: 32,
height: 32,
});
tray.on("tray-clicked", () => {
tray.setMenu([
{ label: "Show Window", action: "show" },
{ type: "separator" },
{ label: "Quit", role: "quit" },
]);
});Related events:
tray-clickedtray-item-clicked
Events and Shutdown Lifecycle
import Electrobun from "electrobun/bun";
Electrobun.events.on("will-navigate", (e) => {
e.response = { allow: true };
});
Electrobun.events.on("open-url", (e) => {
console.log("deep link", e.data.url);
});
Electrobun.events.on("before-quit", async (e) => {
await saveState();
// e.response = { allow: false };
});Quit path guidance:
- Use
before-quitfor cleanup and optional quit cancellation. process.on("exit")is sync-only and should be last-resort.- Linux caveat: some system-initiated quit paths may not currently fire
before-quit; programmatic quit (Utils.quit/process.exit) is reliable.
Updater
import { Updater } from "electrobun/bun";
const local = await Updater.getLocalInfo();
const update = await Updater.checkForUpdate();
if (update.updateAvailable) {
await Updater.downloadUpdate();
}
if (Updater.updateInfo()?.updateReady) {
await Updater.applyUpdate();
}Guidance:
- Keep
release.baseUrland artifact uploads aligned by channel/os/arch. - Patching attempts incremental path first and falls back to full bundle when needed.
Utils
import { Utils } from "electrobun/bun";
Utils.moveToTrash(path);
Utils.showItemInFolder(path);
Utils.openExternal("https://example.com");
Utils.openPath("/path/to/file.pdf");
const paths = await Utils.openFileDialog({
startingFolder: Utils.paths.home,
allowedFileTypes: "png,jpg",
canChooseFiles: true,
canChooseDirectory: false,
allowsMultipleSelection: true,
});
const { response } = await Utils.showMessageBox({
type: "question",
title: "Confirm",
message: "Continue?",
buttons: ["Yes", "No"],
defaultId: 1,
cancelId: 1,
});
Utils.showNotification({ title: "Done", body: "Task complete", silent: false });
Utils.clipboardWriteText("hello");
const text = Utils.clipboardReadText();
const formats = Utils.clipboardAvailableFormats();
Utils.quit();Persistence paths:
Utils.paths.userDataUtils.paths.userCacheUtils.paths.userLogs
Do not write runtime data into bundle resource paths.
GlobalShortcut
import { GlobalShortcut } from "electrobun/bun";
GlobalShortcut.register("CommandOrControl+Shift+Space", () => {
console.log("shortcut fired");
});
GlobalShortcut.isRegistered("CommandOrControl+Shift+Space");
GlobalShortcut.unregister("CommandOrControl+Shift+Space");
GlobalShortcut.unregisterAll();Screen
import { Screen } from "electrobun/bun";
const primary = Screen.getPrimaryDisplay();
const all = Screen.getAllDisplays();
const cursor = Screen.getCursorScreenPoint();Session
import { Session } from "electrobun/bun";
const session = Session.fromPartition("persist:myapp");
// or Session.defaultSession
const cookies = session.cookies.get({ domain: "example.com" });
session.cookies.set({ name: "token", value: "abc", domain: "example.com", secure: true });
session.cookies.remove("https://example.com", "token");
session.cookies.clear();
session.clearStorageData(["cookies", "localStorage"]);Use explicit partition naming for account isolation and predictable cookie/storage behavior.
Window and Webview APIs
Contents
- BrowserWindow constructor and common methods
- BrowserView methods and events
<electrobun-webview>tag- Navigation rules and event behavior
BrowserWindow
import { BrowserWindow } from "electrobun/bun";
const win = new BrowserWindow({
title: "My App",
url: "views://mainview/index.html",
frame: { width: 1200, height: 800, x: 100, y: 100 },
titleBarStyle: "default", // "default" | "hidden" | "hiddenInset"
transparent: false,
sandbox: false, // use true for untrusted content
partition: "persist:main",
preload: "views://mainview/preload.js",
rpc: myRPC,
styleMask: {
Titled: true,
Closable: true,
Miniaturizable: true,
Resizable: true,
},
});Common methods:
setTitle,close,focusminimize/unminimize/isMinimizedmaximize/unmaximize/isMaximizedsetFullScreen/isFullScreensetAlwaysOnTop/isAlwaysOnTopsetPosition,setSize,setFramegetFrame,getPosition,getSize
Window events:
closeresizemovefocus
Default webview:
const webview = win.webview;BrowserView
Access patterns:
win.webviewBrowserView.getById(id)BrowserView.getAll()new BrowserView({...})for advanced use cases
import { BrowserView } from "electrobun/bun";
webview.loadURL("views://mainview/page.html");
webview.loadHTML({ html: "<h1>Hello</h1>" });
webview.executeJavascript('document.title = "new"');
webview.openDevTools();
webview.closeDevTools();
webview.toggleDevTools();
webview.findInPage("search term", { forward: true, matchCase: false });
webview.stopFindInPage();Navigation rules:
webview.setNavigationRules([
"^*", // block all
"*://trusted.com/*", // allow trusted.com
"^http://*", // block non-HTTPS
]);Rule semantics:
- Glob-style patterns
- Prefix
^means block rule - Last matching rule wins
- If no rule matches, navigation is allowed
Built-in RPC helper:
const title = await webview.rpc.request.evaluateJavascriptWithResponse({
script: "document.title",
});BrowserView events:
will-navigatedid-navigatedid-navigate-in-pagedid-commit-navigationdom-readynew-window-opendownload-starteddownload-progressdownload-completeddownload-failed
will-navigate note:
- Navigation allow/block decision is made in native code based on
setNavigationRules. - Event is informational by the time it fires.
<electrobun-webview> (OOPIF)
Custom tag for process-isolated nested webviews.
<electrobun-webview
id="child-webview"
src="https://example.com"
partition="persist:isolated"
sandbox
style="width: 100%; height: 500px;"
></electrobun-webview>Common attributes:
src,html,preload,partition,sandboxtransparent,hidden,passthroughEnabled,delegateMode
Common methods:
loadURL,goBack,goForward,reloadcanGoBack,canGoForwardsetNavigationRulescallAsyncJavaScripton,off
Host messaging from preload:
// preload script in nested webview context
window.__electrobunSendToHost({ type: "click", x: 10, y: 20 });
// host page
document
.getElementById("child-webview")
.on("host-message", (e) => console.log(e.detail));Security posture for nested webviews:
- Use
sandboxfor untrusted content. - Add explicit navigation allowlists.
- Validate all host-message payloads.
Related skills
How it compares
Pick electrobun-best-practices when bundle size and TypeScript-only tooling matter more than Electron's rendering consistency guarantees.
FAQ
What runtime does electrobun-best-practices use for the main process?
electrobun-best-practices targets Electrobun with Bun as the main-process runtime and bundler. The renderer uses the host OS system webview—WKWebView on macOS, WebView2 on Windows, and WebKitGTK on Linux—keeping bundles small compared to Chromium-based frameworks.
How does electrobun-best-practices handle Linux rendering quirks?
electrobun-best-practices recommends setting bundleCEF to true in electrobun.config.ts on Linux and opening BrowserWindow instances with renderer set to cef when GTK WebKit limitations affect layering. CI matrix builds on native runners per OS are the recommended release path.