electron/electron
github.com/electron/electron ↗ · 2937 files · 16.0 MB
:memo: Available Translations: 🇨🇳 🇧🇷 🇪🇸 🇯🇵 🇷🇺 🇫🇷 🇺🇸 🇩🇪.
Overview
Electron is the framework that embeds Chromium and Node.js into a single binary so apps can be built with JavaScript, HTML, and CSS. This repo is the framework itself: a C++ core in shell/ layered on the Chromium content API, a per-process JavaScript API layer in lib/ that is bundled by rolldown and baked into the binary, ~200 maintained patches to Chromium/Node/V8 under patches/, and a Mocha-based spec suite in spec/ that runs inside the built binary.
System map
Electron is a Chromium content-API embedder: platform entry points (shell/app/electron_main_*.cc) create an ElectronMainDelegate that plugs Electron's content clients into content::ContentMain. The browser (main) process lives in shell/browser, renderers in shell/renderer, shared code in shell/common, and a separate utility-process client in shell/utility plus shell/services/node. On top of the C++ core sits a TypeScript layer in lib/ that is bundled per execution context (browser, renderer, worker, webview, isolated_renderer, node, utility) and compiled into the binary. Build is GN/Ninja driven by BUILD.gn and filenames.gni, with dependency versions pinned in DEPS and upstream modifications carried as patch queues in patches/.
- Each platform has a thin main() — shell/app/electron_main_linux.cc line 49 — that hands off to content::ContentMain with an ElectronMainDelegate; the delegate implements BasicStartupComplete, PreSandboxStartup, PreBrowserMain and the CreateContent*Client factory methods.
- The browser-process composition root is ElectronBrowserMainParts: its constructor wires BrowserProcessImpl, NodeBindings, ElectronBindings and the Browser singleton, and PostEarlyInitialization() creates the V8 isolate via JavascriptEnvironment, initializes Node, and loads the app package.
- ElectronMainDelegate::CreateContentRendererClient selects between ElectronSandboxedRendererClient and ElectronRendererClient based on IsSandboxEnabled(command_line), so the sandbox decision happens per child-process launch.
- The JS layer in lib/ is compiled into seven self-contained bundles (browser, renderer, worker, webview, isolated_renderer, node, utility) by build/bundle/bundle.mjs using rolldown, driven by the per-target flags in build/bundle/targets.mjs.
- Native modules are exposed to JS through process._linkedBinding: node_bindings.cc declares the binding names in the ELECTRON_BROWSER_BINDINGS / ELECTRON_COMMON_BINDINGS / ELECTRON_RENDERER_BINDINGS macros, and each C++ file registers itself with NODE_LINKED_BINDING_CONTEXT_AWARE (e.g. electron_browser_app in electron_api_app.cc).
- The 'electron' npm module object is assembled lazily: lib/browser/api/exports/electron.ts calls defineProperties over commonModuleList + browserModuleList, where each entry is { name, loader } with a deferred require().
- require('electron'), 'electron/main', 'electron/renderer', 'electron/common' and 'electron/utility' are module aliases installed by lib/common/init.ts via fake entries in Module._cache and a Module._resolveFilename override.
- Dependencies are pinned in DEPS: chromium_version 155.0.8046.0, node_version v24.21.0, plus squirrel.mac, sparkle, nan and others; gclient applies the patch queue because 'apply_patches': True.
- patches/config.json maps 13 patch directories (chromium, boringssl, devtools_frontend, ffmpeg, v8, node, nan, libaddressinput, perfetto, squirrel.mac, webrtc, sqlite, angle) onto target repos under src/.
- Build configuration is GN: BUILD.gn (~2200 lines) defines the electron targets including the electron_fuses action that generates fuses.h/fuses.cc from build/fuses/fuses.json5; per-build args live in build/args/{all,testing,release}.gn.
- If no app is supplied, resources/default_app.asar provides a fallback app (default_app/) that parses CLI args, can open a file/URL/package directory, prints --version/--abi, or starts a REPL with lazy Electron builtins.
- The published npm package (npm/) is a thin downloader: index.js reads path.txt for the executable name, supports ELECTRON_OVERRIDE_DIST_PATH, and runs install.js to fetch the binary.
Feature catalog
Electron's public surface is a set of lazily-loaded modules on the 'electron' export, split by process: ~40 browser-process modules, 2 common modules, 6 renderer modules and a small utility-process set. Highlights include the windowing stack (BrowserWindow/BaseWindow/WebContentsView), ipcMain/ipcRenderer messaging, session/networking (net, protocol, session), OS integrations (Tray, Menu, Notification, globalShortcut, powerMonitor, autoUpdater), and newer additions like utilityProcess, ServiceWorkerMain, sharedTexture and a local-AI handler. Binary-level behavior toggles called fuses let distributors flip security-relevant features post-build.
- browserModuleList enumerates the main-process API surface: app, autoUpdater, BaseWindow, BrowserView, BrowserWindow, clipboard, contentTracing, crashReporter, desktopCapturer, dialog, globalShortcut, ipcMain, inAppPurchase, Menu/MenuItem, MessageChannelMain, nativeTheme, net, netLog, Notification, powerMonitor, powerSaveBlocker, protocol, pushNotifications, safeStorage, screen, ServiceWorkerMain, session, sharedTexture, ShareMenu, systemPreferences, TouchBar, Tray, utilityProcess, View, webContents, WebContentsView, webFrameMain and native-bound ImageView.
- The renderer-side module list is deliberately small — contextBridge, crashReporter, ipcRenderer, sharedTexture, webFrame, webUtils — while common modules (nativeImage, shell) load on both sides.
- The net module exposes request() (ClientRequest), fetch() (delegated to session.defaultSession.fetch), resolveHost(), isOnline()/online and a WebSocket implementation, and refuses to run before app ready.
- protocol.handle() registers a Fetch-API-style handler per scheme — using registerProtocol for custom schemes and interceptProtocol for http/https/file — converting ProtocolRequest into a Request and the returned Response back to a Chromium loader callback.
- Global protocol functions (registerFileProtocol, interceptBufferProtocol, etc.) are deprecated-style shims that forward to session.defaultSession.protocol.
- session provides fromPartition/fromPath factories, a defaultSession getter, fetch(), setDisplayMediaRequestHandler (with a macOS native-picker fake source id window:-4:N), registerPreloadScript, extensions loading, and registerLocalAIHandler which unwraps a utilityProcess handle.
- utilityProcess.fork() spawns a child running a JS entry script via the native electron_browser_utility_process._fork binding, validating stdio ('pipe'|'inherit'|'ignore' triples, stdin must be 'ignore') and exposing postMessage/kill/pid/stdout/stderr.
- autoUpdater has three platform implementations under lib/browser/api/auto-updater/: auto-updater-native.ts (macOS Squirrel), squirrel-update-win.ts and auto-updater-msix.ts/msix-update-win.ts (Windows).
- ElectronBindings::BindProcess extends the Node process object with Electron methods: crash(), hang(), getCreationTime, getHeapStatistics, getBlinkMemoryInfo, getSystemMemoryInfo, getCPUUsage, getProcessMemoryInfo (browser only), plus takeHeapSnapshot, setFdLimit (POSIX) and activateUvLoop.
- Fuses are post-build toggles baked into the binary: run_as_node, cookie_encryption, node_options, node_cli_inspect, embedded_asar_integrity_validation, only_load_app_from_asar, load_browser_process_specific_v8_snapshot, grant_file_protocol_extra_privileges, wasm_trap_handlers — defined in build/fuses/fuses.json5 and consumed via electron::fuses::* (e.g. IsOnlyLoadAppFromAsarEnabled gates the app-search list).
- The utility process gets its own mini API set (net, local-ai-handler, language-model-utility) via lib/utility/api/module-list.ts, backed by the NodeService in shell/services/node.
- BrowserWindow forwards most content operations (loadURL, loadFile, reload, send, openDevTools, capturePage, setBackgroundThrottling, inspectElement/SharedWorker/ServiceWorker) straight to its webContents, and manages a _browserViews list via addBrowserView/setTopBrowserView.
Data flow
App code enters through a package.json discovered in resources/{app.asar,app,default_app.asar}; the browser process creates a Node environment, loads the bundled browser init script which in turn loads the app's main entry. Renderer↔browser communication rides a mojo interface (mojom::ElectronApiIPC) into a C++ dispatch layer that emits events on the JS ipcMain/ipcMainInternal objects; the reverse direction emits on ipcRenderer via a hidden 'ipcNative' context binding. Network data flows through Chromium URL loader factories, with protocol.handle() bridging Fetch-API Request/Response objects and net.fetch routing through the session's network context. Preload scripts are pushed to the renderer ahead of navigation in startup data rather than fetched by sync IPC.
- App discovery: LoadAppPackage() reads package.json from resources/app.asar, then resources/app, then resources/default_app.asar (only app.asar when the only_load_app_from_asar fuse is on), applies name/productName/version/desktopName, and returns {path, main, esm, v8Flags}.
- PostEarlyInitialization() stashes the AppPackage as a v8 private value 'appPackage' on the Node context global; lib/browser/init.ts retrieves it via getHiddenValue, calls app.setAppPath, then loads the entry with Module._load (CJS) or runEntryPointWithESMLoader (ESM, when type=module or .mjs).
- node_bindings_->JoinAppCode() blocks browser startup until init.ts signals process.appCodeLoaded(), which init.ts invokes after the main module loads (or after the ESM loader settles); this is what synchronizes Chromium startup with the app's JS.
- Renderer→main IPC: the mojom::ElectronApiIPC associated interface delivers Message/Invoke/MessageSync/ReceivePostMessage/MessageHost to ElectronApiIPCHandlerImpl, which resolves the Session from the RenderFrameHost's BrowserContext, builds an api::IpcMainEvent, and calls ipc_dispatch::Message/Invoke which emit on the JS objects registered via electron_browser_ipc_dispatch.setup({ipcMain, ipcMainInternal}).
- ipcMain.handle registrations live in IpcMainImpl._invokeHandlers (a Map keyed by channel) which throws on duplicate handlers; handleOnce wraps handle + removeHandler.
- Main→renderer IPC: EmitIPCEvent in electron_ipc_native.cc looks up a hidden 'ipcNative' private binding on the v8 context (set by the renderer IPC setup), wraps {sender, ports} into an event object, and emits the channel on ipcRenderer or ipcRendererInternal inside a node::CallbackScope when a Node env exists.
- Internal control messages ride reserved channels in the IPC_MESSAGES const enum (e.g. BROWSER_PRELOAD_ERROR re-emitted as 'preload-error' on webContents, BROWSER_GET_PROCESS_MEMORY_INFO returning the calling frame's own renderer pid stats) handled by lib/browser/rpc-server.ts.
- Preload flow: the browser pushes preload paths into RendererStartupData ahead of navigation (WebContents::MaybeSendRendererStartupData); ElectronRendererClient::DidCreateScriptContext creates a per-frame Node environment, defers document loading (LoaderFreezeMode::kStrict), and lib/renderer/init.ts loads CJS preloads via Module._load and .mjs preloads via runEntryPointWithESMLoader, then calls appCodeLoaded which triggers UndeferLoad.
- protocol.handle() converts the native ProtocolRequest into a WHATWG Request — rebuilding uploadData (rawData/file/stream/blob chunk types, blob data pulled via session.defaultSession.getBlobData) into a ReadableStream body — then converts the handler's Response back, with a fast path (relayUntouchedFetch) that hands an untouched net.fetch URL loader straight to Chromium instead of streaming through JS.
- net.fetch/session.fetch go through fetchWithSession in net-fetch.ts into Chromium's network stack under the session's NetworkContext (shell/browser/net/), so requests share the session's cookies, proxy and cache.
- Node module resolution is fenced: lib/node/init.ts overrides Module._nodeModulePaths so modules inside process.resourcesPath can only resolve node_modules inside the app, and patches child_process.fork to set ELECTRON_RUN_AS_NODE=1 (refusing outright when the runAsNode fuse is off).
- The uv loop is demand-driven: lib/common/init.ts wraps process.nextTick, setImmediate, setTimeout and setInterval to call process.activateUvLoop() (bound to ElectronBindings::ActivateUVLoop, which uv_async_sends to OnCallNextTick) so Node callbacks fire even though Chromium owns the main loop.
Behavior traces
Three flows define the system's shape. (1) Cold start: platform main → ElectronMainDelegate hooks → ElectronBrowserMainParts builds V8+Node, loads the app package, runs the bundled init which loads the app's main script, then Chromium's BrowserMain continues. (2) ipcRenderer.invoke: mojo message → ElectronApiIPCHandlerImpl → IpcMainEvent → JS ipcMain handler → reply through a ReplyChannel. (3) protocol.handle: a renderer request hits the session's protocol registry, calls the JS handler with a Request, and streams the Response back to the loader.
- Browser-process startup order: main() → content::ContentMain → ElectronMainDelegate::BasicStartupComplete (env vars ELECTRON_ENABLE_STACK_DUMPING/ELECTRON_DISABLE_SANDBOX, RegisterPathProvider, root-without-sandbox fatal check on Linux) → PreSandboxStartup (logging, crash keys, ResourceBundle for subprocesses, --allow-file-access-from-files) → PreBrowserMain (FieldTrialList, InitializeFeatureList, InitializeMojoCore).
- Startup continued: ElectronBrowserMainParts::PostEarlyInitialization sets up WASM trap handlers (fuse-gated), creates JavascriptEnvironment, node_bindings_->Initialize → CreateEnvironment → sets unhandled_rejections='warn-with-error-code' → ElectronBindings::BindTo → LoadAppPackage → node_bindings_->LoadEnvironment (runs the browser bundle = lib/browser/init.ts) → JoinAppCode → re-initializes the FeatureList so app-set command-line switches take effect.
- lib/browser/init.ts order: require common init (module aliases, uv-loop wrapping) → set EventEmitter prototype on the native binding → install uncaughtException→dialog.showErrorBox handler → map process.exit→app.exit → require ipc-dispatch, rpc-server, guest-view-manager → read 'appPackage' hidden value → app.setAppPath → pre-populate protocol/service-worker-main/web-contents/web-frame-main modules → default window-all-closed→app.quit handler → apply v8Flags → load main entry → appCodeLoaded.
- new BrowserWindow(): JS constructor reaches the native object from process._linkedBinding('electron_browser_window'), whose _init freeze-wraps id, merges setBounds args with getBounds, re-emits focus/blur on app as 'browser-window-focus/blur', debounces 'unresponsive' (50ms; 5s during close), tracks _browserViews, emits 'browser-window-created' on app, then loadURL etc. forward to webContents.
- Renderer script-context flow: Blink creates a script context → ElectronRendererClient::DidCreateScriptContext → ShouldLoadPreload gate → first-time NodeBindings::Initialize/SetUpIsolate (then restores Blink's WASM streaming callback) → per-frame FrameEnvironment + node::InitializeContext → document load deferred (kStrict) → CreateEnvironment registers UndeferLoad callback → BindTo + BindProcess → preload paths stored as hidden 'preloadPaths'.
- Preload execution order in lib/renderer/init.ts: patch Module._load to warn on 'vm' → override Module.wrapper to keep process/global/Buffer for preloads → common init → read webPreferences (nodeIntegration) → set up node globals only if nodeIntegration → load cjsPreloads synchronously (errors → BROWSER_PRELOAD_ERROR IPC) → esmPreloads via runEntryPointWithESMLoader → onPreloadsLoaded deletes node globals when !nodeIntegration && !contextIsolated → appCodeLoaded → UndeferLoad unfreezes the document.
- ipcRenderer.invoke('ch', ...args): the renderer binding sends ElectronApiIPC.Invoke over the associated mojo channel → ElectronApiIPCHandlerImpl::Invoke resolves api::Session from the frame's BrowserContext → MakeIPCEvent builds api::IpcMainEvent carrying the mojo InvokeCallback (ReplyChannel) → ipc_dispatch::Invoke looks up IpcMainImpl._invokeHandlers['ch'] → return value/promise serialized back through SerializedValue to the renderer promise.
- If the Session or WebContents wrapper is gone when an Invoke arrives, MakeIPCEvent still invokes the reply callback with an error via ReplyChannel::SendError so the renderer promise rejects instead of hanging.
- protocol.handle('app', handler) on a custom scheme: registerProtocol stores the callback → a request arrives as ProtocolRequest → convertToRequestBody rebuilds uploadData → a Request is constructed (initiatorOrigin attached as own prop, 'null' origin header stripped) → await handler(req) → validateResponse → if the Response is an untouched net.fetch result, relayUntouchedFetch holds the URL loader and streams prefix+remainder natively; otherwise res.body is wrapped with Readable.fromWeb → cb({data, headers, statusCode, statusText, mimeType}).
- utilityProcess.fork flow: JS validates options (execArgv array, serviceName string, stdio triple with stdin forced 'ignore') → native _fork launches a utility process → the returned handle emits 'stdout'/'stderr' events carrying fds that JS wraps in Socket→PassThrough pipes → 'exit' nulls the handle and tears down streams.
- ELECTRON_RUN_AS_NODE flow: lib/node/init.ts patches child_process.fork to inject ELECTRON_RUN_AS_NODE=1 (and helperExecPath on macOS); on Linux, spawn/spawnSync are wrapped to forward CRASHDUMP_SIGNAL_FD/CRASHPAD_HANDLER_PID env pairs to such children; shell/app/node_main.cc detects the env var and runs plain Node instead of Chromium.
- V8 snapshot integrity: when the embedded_asar_integrity_validation fuse is on, a validator registered via gin::SetV8SnapshotValidator CHECK-fails startup unless the snapshot's SHA-256 equals the compiled-in snapshot_checksum::kChecksum.
Testing signals
Tests are Mocha specs in spec/ (api-*-spec.ts per module plus feature specs like asar/fuses/esm/crash) executed inside the built Electron binary — spec/index.js is literally run as the app's main script by script/spec-runner.js. Assertions use chai with dirty-chai and chai-as-promised; CI shards by per-file weights and records timing artifacts. Coverage is strongest on the JS-facing API surface; C++ internals are exercised indirectly, and there are no unit tests for shell/ outside the in-binary specs.
- npm test → script/spec-runner.js, which installs spec node_modules when the spec hash changed, generates electron.d.ts if missing, then launches the built Electron binary with spec/ as the app.
- spec/index.js is the test app's main: it registers privileged test schemes ('app', 'zoom', 'sw', 'cors', 'stream', ...), appends js-flags=--expose_gc and enable-features switches, loads Mocha with forbidOnly+3 retries on CI, TAP reporter by default (MOCHA_REPORTER/MOCHA_MULTI_REPORTERS override), 30s default timeout (MOCHA_TIMEOUT).
- Spec discovery picks files matching /-spec\.[tj]s$/ under spec/ via get-files.ts; MOCHA_GREP/MOCHA_INVERT and --files allow sharding; npm_config_match filters by name.
- disabled-tests.json is a title-exact skip list enforced by a mocha beforeEach root hook; per-suite beforeAll kills orphaned Electron processes (pgrep/wmic/proc scan filtered by execPath and PID-descent) between spec files.
- On CI the runner writes spec/artifacts/spec-timings.json (per-file wall time keyed by platform/arch/mas/sanitizer) consumed by script/gen-spec-weights.js; script/spec-weights.json already holds per-file shard weights.
- spec/package.json pins the harness deps — mocha ^10, chai + chai-as-promised + dirty-chai, mocha-junit-reporter, ws, busboy — plus @electron-ci/* native-addon fixtures (dialog-helper, echo, is-valid-window, object-wrap, uv-dlopen, virtual-display) and @electron/fuses.
- ELECTRON_REQUIRE_TESTING_BINDINGS=1 makes the run fail early if testing bindings are unavailable, distinguishing testing builds from release binaries.
- electron.d.ts is generated from docs API markdown (create-api-json.mjs + @electron/typescript-definitions) and validated by a smoke test: spec/ts-smoke/runner.js compiles fixture TS against the definitions; the same generation runs as a docs lint step.
- Fixtures cover asar archives (test.asar, recursive-asar), crash-cases, devtools-extensions, native-addon builds, ESM variants and webview pages under spec/fixtures; helper modules live in spec/lib (http-server-helpers, codesign-helpers, window-helpers, ...).
- Linting is a first-class gate: npm run lint chains script/lint.js (oxlint for JS/TS, cpplint-style for C++/ObjC, python, gn), oxfmt format check, clang-format on shell/, and a docs pipeline that lints code blocks in markdown and validates api-history YAML blocks against docs/api-history.schema.json.
- CI splits work across pipeline workflows: pipeline-segment-electron-build.yml, pipeline-segment-electron-test.yml (plus a -64k variant), pipeline-electron-lint.yml, apply-patches.yml, and per-OS publish workflows (linux/macos/windows-publish.yml); audit workflows publish findings as an 'audit-results.md' artifact.
- default_app/ has no dedicated spec file — its CLI behaviors (REPL, --version, package loading) are only covered indirectly through specs that launch Electron with app paths; the argument parser itself is untested in isolation. (inferred)
Risk map
The largest risks are the security boundary between web content and Node, the breadth of the IPC surface, and the patch-queue maintenance burden. Sandbox/safety defaults are computed from webPreferences in one place (web_contents_preferences.cc) so changes there have wide blast radius; several features (fuses, ELECTRON_RUN_AS_NODE, preload privilege, protocol handlers) deliberately trade security for capability; and ~200 patches to Chromium/Node/V8 must be re-applied and merged on every dependency roll.
- Sandbox defaults are derived, not explicit: RendererProcessPreferences::From marks a renderer sandboxed unless nodeIntegration or nodeIntegrationInWorker is set (or webPreferences.sandbox overrides), and AppendCommandLineSwitches injects --no-sandbox/--no-zygote for unsandboxed renderers — a single pref change flips process isolation.
- ELECTRON_DISABLE_SANDBOX unconditionally appends --no-sandbox at BasicStartupComplete, and ELECTRON_RUN_AS_NODE turns the whole binary into Node — both are env-var-controlled capabilities attackers can sometimes influence.
- Preload scripts run with Node globals even in isolated worlds; the Module.wrapper hack in lib/renderer/init.ts must stay in sync with the bundle's inject list in build/bundle/bundle.mjs, and mistakes leak process/Buffer into the main world (the deletion path only runs for non-context-isolated renderers).
- The IPC surface trusts channel strings end-to-end: internal channels share the same wire as app channels (ipcMainInternal vs ipcMain separated only by the 'internal' bool on the mojo call), so a confused-deputy bug in ipc_dispatch or event.type checks (e.g. rpc-server.ts guards event.type !== 'frame') would let renderer content reach privileged handlers.
- process._linkedBinding exposes the entire native binding table (ELECTRON_*_BINDINGS) to any code running with Node in the renderer when nodeIntegration is on — including process.crash() and process.hang() which deliberately crash/hang the process.
- Fuses change security posture post-build without recompilation: grant_file_protocol_extra_privileges defaults to '1' (on) and run_as_node/node_options/node_cli_inspect default on; embedded_asar_integrity_validation and only_load_app_from_asar default off, so integrity guarantees are opt-in.
- protocol.handle bridges raw request data into Fetch objects: uploadData chunk types (rawData/file/stream/blob) are reassembled by hand and blob bytes come from session.defaultSession regardless of the requesting session (noted in the code as relying on globals) — a subtle cross-session data path.
- <webview> webPreferences are parsed from a DOM attribute string via parse-features-string, and guest-view-manager merges them with embedder-inherited security prefs; malformed or malicious attribute values are a parsing/injection surface, mitigated by the inheritedWebPreferences allowlist.
- security_warnings.cc exists specifically to nag developers about insecure configurations (insecure content, disabled webSecurity, etc.) in devtools — implying the footguns are common enough to need runtime warnings, gated off in tests via ELECTRON_DISABLE_SECURITY_WARNINGS.
- The uncaughtException handler in lib/browser/init.ts swallows main-process crashes into a GUI error box (showErrorBox) instead of exiting — a deliberate 'don't quit' policy that can mask fatal errors in production apps.
- patches/ carries ~200 patches across 13 upstream repos (chromium, node, v8, angle, sqlite, webrtc, boringssl, ffmpeg, ...); each Chromium roll (DEPS chromium_version) requires 3-way merging the queue, and patch rot in security-relevant files is a recurring fragility the repo's own docs warn about.
- spec/index.js kills processes matching the Electron execPath between spec files — on macOS via pgrep -f and on Linux by scanning /proc/*/exe — which would terminate any co-located Electron app sharing the binary path if run outside CI.
Extension points
New APIs plug in at three layers: a C++ binding registered via NODE_LINKED_BINDING_CONTEXT_AWARE plus a name in the ELECTRON_*_BINDINGS macros, a JS wrapper added to the appropriate lib/<proc>/api/module-list.ts, and a docs/api/*.md page that generates the .d.ts. Apps extend Electron through protocol.handle/registerSchemesAsPrivileged, session partitions and preload-script registration, ipcMain/ipcRenderer channels, utilityProcess.fork, and (for embedders) build-time fuses and GN buildflags. docs/development/creating-api.md documents the recipe.
- To add a main-process module: add { name, loader } to browserModuleList in lib/browser/api/module-list.ts (alphabetical, with a matching module-keys update noted in the file's TODO), implement lib/browser/api/<name>.ts, and it appears on require('electron') via defineProperties.
- To add a native binding: register Initialize() with NODE_LINKED_BINDING_CONTEXT_AWARE(electron_<proc>_<name>, Initialize) at file scope and add the name to the matching ELECTRON_*_BINDINGS macro in node_bindings.cc; JS reaches it via process._linkedBinding('electron_<proc>_<name>').
- docs/development/creating-api.md is the canonical recipe for new APIs; the docs page's YAML 'history' front-matter is schema-validated and feeds electron.d.ts generation via create-api-json.mjs + electron-typescript-definitions.
- protocol.registerSchemesAsPrivileged (must run before app ready, per spec/index.js usage) + session.protocol.handle/unhandle/isProtocolHandled let apps mint custom URL schemes with privileges (standard, secure, stream, corsEnabled, supportFetchAPI, allowServiceWorkers).
- session.fromPartition/fromPath create isolated storage/network contexts; registerPreloadScript registers frame- or service-worker-scoped preloads (superseding getPreloads/setPreloads which are deprecation shims).
- session.extensions.loadExtension (Chromium extensions support, ENABLE_ELECTRON_EXTENSIONS buildflag) is the app-facing extension point for MV2-style extensions; old session.loadExtension calls are rerouted with deprecation warnings.
- ipcMain.handle/on and ipcRenderer.invoke/send are the app-level RPC extension point; MessageChannelMain/MessagePortMain support transferable ports, and ipcMainServiceWorker covers service-worker targets.
- utilityProcess.fork(modulePath, args, opts) is the supported way to run app JS in a dedicated process with postMessage messaging — replacing child_process.fork when the runAsNode fuse is off.
- GN buildflags (buildflags/buildflags.gni) gate major subsystems — ENABLE_ELECTRON_EXTENSIONS, ENABLE_BUILTIN_SPELLCHECKER, ENABLE_PDF_VIEWER, ENABLE_PRINTING, ENABLE_PLUGINS — and are exposed to JS at runtime via electron_common_features binding (features.cc).
- Fuses are the distributor-facing extension mechanism: build/fuses/fuses.json5 is the schema (with a 'never remove, mark removed r' rule and _version field), generated into fuses.h/fuses.cc by build/fuses/build.py during the electron_fuses GN action; the @electron/fuses npm package flips them in shipped binaries.
- New webview-exposed behaviors plug into the GUEST_VIEW_MANAGER_* internal channels plus the syncMethods/asyncMethods/properties tables in lib/common/web-view-methods.ts and the event list in lib/browser/web-view-events.ts.
- New patch targets are added by extending patches/config.json with a {patch_dir, repo} pair — the queue is applied by e sync / apply-patches.yml and exported by e patches <target>.
Contributor handoff
This is a Chromium-tree project, not a standalone repo: it lives at src/electron inside a Chromium checkout, builds with GN/Ninja via @electron/build-tools ('e' commands), and tests run the built binary against spec/. Follow the pinned rules: no npx, no force-push conventions, patches are fixed not recreated, C++ is Chromium-style, JS/TS is oxlint+oxfmt. Read the root CLAUDE.md and docs/development/* before editing; run lint-staged-adjacent checks (oxlint, clang-format, gen-filenames) since filenames.gni is generated.
- Read first: root CLAUDE.md (build commands, patch rules, cppgc/GC rules, PR conventions), docs/development/source-code-directory-structure.md, docs/development/creating-api.md, docs/development/testing.md, then BUILD.gn/DEPS/filenames.gni for build wiring.
- Never use npx — it can fetch arbitrary packages; run binaries via node_modules/.bin/<tool> or yarn <tool>. This applies to authored scripts too.
- Build/test loop: e init/e use/e sync/e build/e start/e test (from @electron/build-tools); inside the repo, npm test runs script/spec-runner.js and npm run lint runs the full lint chain (oxlint, oxfmt, clang-format, docs lint, gn check).
- filenames.gni and filenames.auto.gni are generated: lint-staged runs script/gen-filenames.mts on *.{js,mjs,ts,mts,d.ts} changes, and bundle.mjs --print-graph produces the module list — hand-editing them is a trap.
- Patch workflow: fix existing patches rather than adding new ones, preserve TODO(name) attribution, edit the target repo then 'e patches <target>' to export; for CI patch conflicts fetch the update-patches.patch artifact from the Apply Patches run instead of a local e sync --3.
- Spec-writing rules (spec/CLAUDE.md): never expect() inside 'data'/'message'/'exit' listeners, wait for expected output not 'exit', accumulate stdout before asserting (stderr races), and register forked children with deferKillUtilityProcess so the global afterEach reaps them.
- C++ conventions follow Chromium style enforced by clang-format (script/run-clang-format.py); cppgc/Oilpan rules are strict — cppgc::MakeGarbageCollected only, Member/WeakMember edges, no destructors touching other GC objects, GCPA-managed callbacks via WeakCellFactory.
- API changes need synchronized updates across lib/<proc>/api/module-list.ts (+module-keys), the C++ binding registration, docs/api/*.md with YAML history blocks (lint:api-history validates against docs/api-history.schema.json), docs/breaking-changes.md for removals, and regenerated electron.d.ts.
- PR conventions: body must end with a 'Notes:' consumer-facing line ('Notes: none' if internal); label semver/{none,patch,minor,major} and target/{N}-x-y backports — bug fixes to all active branches except oldest, security fixes including oldest, features main-only.
- Known traps: the renderer's Module.wrapper hack must mirror build/bundle/bundle.mjs inject entries; the 'vm' module is unusable in renderers; process.exit is remapped to app.exit; renderer node globals are deleted unless nodeIntegration/contextIsolation; unhandled rejections are set to 'warn-with-error-code' not crash.
- Safe edit boundaries: lib/ changes are self-contained TS that get bundled — run build/bundle/typecheck.mjs and oxlint; shell/ C++ changes need clang-format and a full build; spec/ changes should mirror the api-<module>-spec.ts naming convention and use spec/lib helpers. (inferred)
- CI artifacts worth knowing: audit workflows upload 'audit-results.md' unarchived (archive_download_url returns raw markdown); the Apply Patches workflow uploads 'update-patches.patch'; spec-timings.json feeds shard balancing.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 526 | 32% |
| C | 438 | 27% |
| C++ | 415 | 25% |
| Markdown | 324 | docs/config |
| HTML | 264 | docs/config |
| TypeScript | 214 | 13% |
| JSON | 138 | docs/config |
| YAML | 78 | docs/config |
Start reading here
Probable source entrypoint: lib/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (98 files)
Repository structure
| Top-level path | Files |
|---|---|
shell/ | 932 |
spec/ | 919 |
docs/ | 474 |
patches/ | 226 |
script/ | 117 |
lib/ | 98 |
.github/ | 92 |
.claude/ | 15 |
Docs found: CLAUDE.mdCONTRIBUTING.mdREADME.mddocs/CLAUDE.mddocs/README.mddocs/api/app.mddocs/api/auto-updater.mddocs/api/base-window.mddocs/api/browser-view.mddocs/api/browser-window.md
Package
package.json — @electron-ci/dev-root v0.0.0-development, 30 declared dependencies.
@azure/storage-blob@datadog/datadog-ci@electron/asar@electron/docs-parser@electron/fiddle-core@electron/github-app-auth@electron/lint-roller@electron/typescript-definitions@hurdlegroup/robotjs@octokit/rest@sentry/cli@types/minimist
CI & config entrypoints
README.md— Repository readmeCLAUDE.md— Claude instructions.env.example— Required env vars.github/workflows/macos-publish.yml— GitHub Actions workflow.github/workflows/linux-publish.yml— GitHub Actions workflow.github/workflows/non-maintainer-dependency-change.yml— GitHub Actions workflow.github/workflows/windows-publish.yml— GitHub Actions workflow.github/workflows/issue-unlabeled.yml— GitHub Actions workflow.github/workflows/audit-branch-ci.yml— GitHub Actions workflow.github/workflows/check-nmv-version.yml— GitHub Actions workflow
Structural health signals
Average score 6.2/10 across 96 analyzed source files; 53 hotspots surfaced.
spec/chromium-spec.ts— 1301 lines, score 4 (Large file, Deep nesting)spec/api-app-spec.ts— 1442 lines, score 4 (Large file, Deep nesting)spec/api-protocol-spec.ts— 1266 lines, score 4 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
56f681cfe08e2026-09-21 — test: collect orphaned MessagePortMain with a precise GC in the close-event spec (#54191)8a6429e414082026-09-21 — build: upgrade to TypeScript 7 (#54172)9baaed782e642026-09-21 — test: load specs as native ES modules (#54171)2774d5761f6c2026-09-21 — test: keep spec sources to erasable syntax and explicit type imports (#54170)1e93884db50f2026-09-21 — fix: remove read-only files with fs.rmSync on Windows (#54180)