Build Configuration
electrobun.config.ts is a TypeScript module in the project root. Hutch loads
its default export with Cottontail and merges it with Electrobun’s defaults.
Use satisfies ElectrobunConfig so unsupported fields and values fail during
development. Electrobun release overrides, tasks, and package-manager selection
belong in hutch.config.ts; the release and external package-manager fields are
optional. This file owns the app, build, tool overrides, and distribution
description.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "My App", identifier: "com.example.my-app", version: "1.0.0", }, build: { mainProcess: "cottontail", cottontail: { entrypoint: "src/bun/index.ts", }, views: { main: { entrypoint: "src/mainview/index.ts", }, }, copy: { "src/mainview/index.html": "views/main/index.html", "src/mainview/index.css": "views/main/index.css", }, }, runtime: { exitOnLastWindowClosed: true, },} satisfies ElectrobunConfig;Run hutch electrobun config --env=dev to inspect the merged configuration
that Hutch will use.
Electrobun Devkit
hutch.config.ts may set electrobun.version to an exact release such as
2.0.0 or 2.1.0-beta.3. That optional pin wins over the npm bootstrap’s
paired release default or a direct Hutch invocation’s floating channel. The
application version in app.version remains independent.
Hutch resolves and verifies the selected release under
~/.hutch/releases/electrobun, then copies its config types and language SDKs
into the generated project .hutch/devkit sysroot. Run
hutch electrobun prepare to materialize or refresh that project copy without
building or advancing a valid floating projection. build, run, and dev
prepare implicitly. Run hutch electrobun update to move an existing exact pin
to the latest stable release and sync the app; use hutch electrobun sync when
you deliberately want an unpinned direct-Hutch project to follow the current
release channel head. See
Project Ownership and the Devkit for
the config responsibility table, generated-state lifecycle, and
external-bundler aliases.
Application Metadata
app is required.
| Field | Type | Purpose |
|---|---|---|
name | string | Display name and base artifact name. |
identifier | string | Platform application identifier, normally reverse-DNS. |
version | string | Application version written into release metadata. |
description | string? | Human-readable description. |
urlSchemes | string[]? | Custom URL schemes on macOS. |
fileAssociations | FileAssociation[]? | Document associations on macOS. |
URL Schemes
Custom URL schemes are currently registered on macOS. Install the built app
in /Applications or ~/Applications so Launch Services can register it.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Link Receiver", identifier: "com.example.link-receiver", version: "1.0.0", urlSchemes: ["link-receiver", "link-receiver-dev"], },} satisfies ElectrobunConfig;Handle the resulting URL in the main process:
import Electrobun from "electrobun/main";
Electrobun.events.on("open-url", (event) => { const url = new URL(event.data.url); console.log(url.protocol, url.pathname);});File Associations
File associations are currently packaged on macOS. Extensions do not include
the leading dot. An optional icon must be an .icns source file.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Document Editor", identifier: "com.example.document-editor", version: "1.0.0", fileAssociations: [ { name: "Example Document", ext: ["example", "example-project"], role: "Editor", icon: "assets/example-document.icns", }, ], },} satisfies ElectrobunConfig;Opened files arrive through open-url as file: URLs.
import Electrobun from "electrobun/main";
Electrobun.events.on("open-url", (event) => { const url = new URL(event.data.url); if (url.protocol === "file:") { console.log("Opened file", decodeURIComponent(url.pathname)); }});Windows and Linux URL schemes and file associations are not yet implemented.
Main Process
build.mainProcess selects the runtime and compiler. It defaults to
"cottontail".
| Value | Configuration | Project build input |
|---|---|---|
cottontail | build.cottontail | src/bun/index.ts |
bun | build.bun | src/bun/index.ts |
zig | build.zig | project build.zig |
rust | build.rust | Cargo.toml, binary target main |
go | build.go | go.mod, package ./src/go |
odin | build.odin | package containing src/odin/main.odin |
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Native Example", identifier: "com.example.native", version: "1.0.0", }, build: { mainProcess: "zig", zig: { version: "0.16.0" }, },} satisfies ElectrobunConfig;Each selected Electrobun release declares default Zig, Rust, Go, and Odin
versions. The matching build.zig.version, build.rust.version,
build.go.version, or build.odin.version field is an exact per-project
override. Hutch reuses exact matching system compilers where supported, or
stores the selected compiler under
~/.hutch/toolchains/<language>/<version>/<platform>.
Native build descriptions remain project-owned:
- Zig:
build.zig; Hutch passes-Delectrobun-sdk=<project>/.hutch/devkit/zig-sdk/electrobun.zig. - Rust:
Cargo.tomlandCargo.lock; configuremanifest(defaultCargo.toml) andbinary(defaultmain). A local dependency may point to.hutch/devkit/rust-sdk. - Go:
go.modandgo.sum; configurepackage(default./src/go). Userequire electrobun v0.0.0andreplace electrobun => ./.hutch/devkit/go-sdkin the module root. - Odin: normal source package;
entrypointdefaults tosrc/odin/main.odin, and Hutch passes the projectedelectrobun_sdkcollection. Nobuild.odinis generated or required.
Hutch invokes the normal project build tool. It does not generate build.zig,
Cargo manifests, Go modules, or a temporary GOPATH.
Hutch builds for the current host platform and architecture. Use native CI runners when producing the macOS, Windows, and Linux release matrix.
Bytecode And Obfuscation
When the main process runs on Cottontail, build.cottontail.bytecode can ship
it as precompiled JavaScriptCore bytecode instead of source. This is a
Cottontail-only option — Bun main processes are unaffected.
There are three levels, and the two “on” levels are deliberately distinct because they carry different trade-offs:
bytecode | Ships | Startup | Source protection | Debuggable | Risk |
|---|---|---|---|---|---|
false (default) | .js | normal | none | yes | — |
true | .jsc + .js | faster (skips parsing) | none | yes | low — falls back to parsing the source |
"obfuscate" | .jsc only | faster | source not shipped | no | see below |
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "My App", identifier: "com.example.my-app", version: "1.0.0", }, build: { mainProcess: "cottontail", cottontail: { entrypoint: "src/bun/index.ts", // Faster startup, source still shipped and debuggable: bytecode: true, // Or protect your source (accept the trade-offs below): // bytecode: "obfuscate", }, },} satisfies ElectrobunConfig;bytecode: true compiles the main process to bytecode so the runtime skips
parsing at launch. Your source is still shipped alongside the bytecode as a
fallback, so Function.prototype.toString returns real source, error stacks
show source frames, and a version mismatch simply reparses the source. This is
the safe “just make my app start faster” choice.
bytecode: "obfuscate" additionally strips the source, so only bytecode is
distributed. Consider the trade-offs before enabling it:
Function.prototype.toStringreturns[native code]for your functions. Some libraries introspect function source (certain dependency-injection, ORM, and validation frameworks) and can break.- Error stacks keep line and column numbers but lose source frames, and no sourcemaps are emitted — shipped-app debugging is degraded.
- There is no source fallback, so a runtime/version mismatch is a hard error rather than a silent reparse.
This is source obfuscation, not encryption: the bytecode ships without your source text, comments, or original identifiers, which makes it materially harder to read than minified JavaScript, but a determined party with the matching Cottontail binary can still disassemble it.
Two things hold regardless of the setting:
- Dev builds ignore
bytecode(hutch electrobun dev/--env=dev) and always ship source, so sourcemaps and the debugger keep working. In watch mode, Hutch rebuilds and relaunches the app from that source. Bytecode applies to canary and stable builds. - The bytecode is generated against the exact Cottontail bundled into your app and regenerated on every build, so it never drifts from the runtime that will execute it.
JavaScript Bundling
Hutch owns the output path, platform, and base format for Cottontail, Bun, and view bundles. The following serializable options are currently forwarded to Cottontail’s build API:
| Option | Type | Applies to |
|---|---|---|
minify | boolean | main process and views |
sourcemap | boolean | "inline" | "external" | "linked" | main process and views |
target | string | string[] | main process and views |
external | string[] | main process and views |
define | Record<string, string> | main process and views |
alias | Record<string, string> | main process and views |
format | "esm" | "cjs" | "iife" | views only |
Config is serialized while Hutch loads it. Function-valued bundler plugins
cannot cross that boundary and are not currently supported in
electrobun.config.ts. Framework templates that need plugin transforms run
their own Vite build and copy its output.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Bundled App", identifier: "com.example.bundled-app", version: "1.0.0", }, build: { cottontail: { entrypoint: "src/bun/index.ts", minify: true, sourcemap: "linked", external: ["optional-native-module"], define: { "process.env.APP_EDITION": '"desktop"', }, alias: { "@shared": "./src/shared/index.ts", }, }, views: { main: { entrypoint: "src/mainview/index.ts", minify: true, sourcemap: "linked", format: "esm", }, }, },} satisfies ElectrobunConfig;Static Files And Output
build.copy maps project-relative source paths to safe relative paths inside
the packaged application resources. buildFolder and artifactFolder are
also project-relative. Nested paths are supported, but absolute paths, empty
paths, and . or .. path components are rejected before Hutch creates,
copies, or removes output files.
| Field | Type | Default |
|---|---|---|
copy | Record<string, string> | {} |
buildFolder | string | build |
artifactFolder | string | artifacts |
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Packaged App", identifier: "com.example.packaged-app", version: "1.0.0", }, build: { copy: { "src/mainview/index.html": "views/main/index.html", "assets": "assets", }, buildFolder: "build", artifactFolder: "artifacts", },} satisfies ElectrobunConfig;Watch Mode
hutch electrobun dev --watch derives watch roots from configured source and
native build inputs, view entrypoints, and copy sources. Add other source
locations with build.watch; suppress generated or high-churn paths with
build.watchIgnore. build, artifacts, and
node_modules are always ignored.
Watch mode rebuilds and relaunches the whole app; it is distinct from webview-only hot module replacement. See Hot Reloading for the main-process, Vite, and Warren reload boundaries.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Watched App", identifier: "com.example.watched-app", version: "1.0.0", }, build: { watch: ["scripts", "vendor/native-library"], watchIgnore: ["**/*.generated.*", "data/cache/**"], },} satisfies ElectrobunConfig;Platform Options
macOS
| Field | Type | Default |
|---|---|---|
codesign | boolean | false |
createDmg | boolean | true |
notarize | boolean | false |
bundleCEF | boolean | false |
bundleWGPU | boolean | false |
defaultRenderer | "native" | "cef" | "native" |
chromiumFlags | Record<string, string | boolean> | none |
entitlements | Record<string, boolean | string | string[]> | JIT defaults |
icons | string | icon.iconset |
An icon source can be an .iconset directory or an Icon Composer .icon
file. notarize requires signing credentials; see Code
Signing.
Windows
| Field | Type | Default |
|---|---|---|
bundleCEF | boolean | false |
bundleWGPU | boolean | false |
defaultRenderer | "native" | "cef" | "native" |
chromiumFlags | Record<string, string | boolean> | none |
autoGrantPermissions | WindowsWebView2Permission[] | [] |
icon | string | none |
autoGrantPermissions applies to every origin in native WebView2 views and
does not affect CEF. Grant only capabilities the application intentionally
delegates to its web content.
Linux
| Field | Type | Default |
|---|---|---|
bundleCEF | boolean | false |
bundleWGPU | boolean | false |
defaultRenderer | "native" | "cef" | "native" |
chromiumFlags | Record<string, string | boolean> | none |
icon | string | none |
flatpak | FlatpakConfig | disabled |
Flatpak mode writes an expanded flatpak-builder manifest and /app payload;
Hutch does not invoke flatpak-builder itself.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Cross Platform App", identifier: "com.example.cross-platform", version: "1.0.0", }, build: { mac: { bundleCEF: true, bundleWGPU: true, defaultRenderer: "cef", codesign: true, notarize: true, icons: "App.icon", }, win: { bundleCEF: true, bundleWGPU: true, defaultRenderer: "cef", autoGrantPermissions: ["camera", "microphone"], icon: "assets/icon.ico", }, linux: { bundleCEF: true, bundleWGPU: true, defaultRenderer: "cef", icon: "assets/icon.png", flatpak: { enabled: true, runtimeVersion: "25.08", }, }, },} satisfies ElectrobunConfig;Chromium Flags
chromiumFlags is used only by CEF. Keys omit the -- prefix. A true value
adds a switch, a string adds a switch with a value, and false suppresses an
Electrobun default with the same name.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "CEF App", identifier: "com.example.cef-app", version: "1.0.0", }, build: { mac: { bundleCEF: true, chromiumFlags: { "show-paint-rects": true, "user-agent": "CEF App/1.0", "disable-gpu": false, }, }, },} satisfies ElectrobunConfig;Development CEF builds automatically select an available loopback debugging
port from 9222 through 9232. Canary and stable builds disable remote
debugging unless "remote-debugging-port" is set to a string port from 1024
through 65535. Set it to false to disable the dev endpoint too.
ELECTROBUN_CEF_REMOTE_DEBUGGING_PORT can override this for one launch. Set it
to a port, or to 0, off, or false to disable the endpoint.
Runtime Values
The runtime object is copied into build.json. exitOnLastWindowClosed
defaults to true; set it to false for tray and background applications.
Additional JSON-compatible values are available through BuildConfig.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Background App", identifier: "com.example.background-app", version: "1.0.0", }, runtime: { exitOnLastWindowClosed: false, serviceEndpoint: "https://api.example.com", },} satisfies ElectrobunConfig;import { BuildConfig } from "electrobun/main";
const config = await BuildConfig.get();console.log(config.runtime?.serviceEndpoint);Build Hooks
Hook paths are relative to the project root. Hutch transpiles each TypeScript or JavaScript file and executes it with the host-platform Cottontail binary.
| Hook | Timing |
|---|---|
preBuild | Before any application build work. |
postBuild | After main process, views, copied assets, and Carrot output are built. |
postWrap | After a non-dev release wrapper is assembled, before final packaging/signing. |
postPackage | After dev output or all release artifacts are complete. |
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Hooked App", identifier: "com.example.hooked-app", version: "1.0.0", }, scripts: { preBuild: "./scripts/pre-build.ts", postBuild: "./scripts/post-build.ts", postWrap: "./scripts/post-wrap.ts", postPackage: "./scripts/post-package.ts", },} satisfies ElectrobunConfig;All hooks inherit the process environment and receive:
ELECTROBUN_BUILD_ENV:dev,canary, orstableELECTROBUN_OS:macos,linux, orwinELECTROBUN_ARCH:x64orarm64ELECTROBUN_BUILD_DIRELECTROBUN_ARTIFACT_DIRELECTROBUN_APP_NAMEELECTROBUN_APP_VERSIONELECTROBUN_APP_IDENTIFIER
postWrap also receives ELECTROBUN_WRAPPER_BUNDLE_PATH. postBuild
receives ELECTROBUN_CARROT_DIR when the build produced a Carrot.
import { cpSync, existsSync } from "node:fs";import { join } from "node:path";
const wrapper = process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH;if (!wrapper) throw new Error("This hook requires a release wrapper");
const source = "assets/release-only";if (existsSync(source)) { cpSync(source, join(wrapper, "Contents", "Resources", "release-only"), { recursive: true, });}The path layout in that example is macOS-specific; branch on ELECTROBUN_OS
for a cross-platform hook.
Release Configuration
release.baseUrl is the public artifact base used by update metadata.
release.generatePatch defaults to true; disable it when a local canary or
stable build should skip downloading and diffing the previous release.
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "Released App", identifier: "com.example.released-app", version: "2.0.0", }, release: { baseUrl: "https://releases.example.com/released-app/", generatePatch: true, },} satisfies ElectrobunConfig;Carrot Packaging
build.carrot produces a Carrot artifact in addition to the standalone app.
Set carrotOnly: true to omit the standalone application. The object accepts
an ID, display name, optional description and mode, dependencies, remote/slate
UI definitions, and file-activator contributions. bunny.carrot.dependencies
declares dependencies used when the same project is distributed through Bunny
Ears.