Never use npx. It is considered dangerous because it can silently fetch and execute arbitrary packages from the registry. Always run binaries through one of these safer mechanisms instead:
- Preferred — spawn the executable directly from
node_modules/.bin/<tool>(or the platform equivalent on Windows). This is whatscript/lint.jsdoes foroxlint. - Acceptable — invoke via
yarn <tool>oryarn run <tool>, which resolves to the locally installed version without the registry fallback thatnpxperforms.
This rule applies to shell commands you run yourself and to any scripts you author or modify in this repo.
Electron is a framework for building cross-platform desktop applications using web technologies. It embeds Chromium for rendering and Node.js for backend functionality.
electron/ # This repo (run `e` commands here)
├── shell/ # Core C++ application code
│ ├── browser/ # Main process implementation (107+ API modules)
│ ├── renderer/ # Renderer process code
│ ├── common/ # Shared code between processes
│ ├── app/ # Application entry points
│ └── services/ # Node.js service integration
├── lib/ # TypeScript/JavaScript library code
│ ├── browser/ # Main process JS (47 API implementations)
│ ├── renderer/ # Renderer process JS
│ └── common/ # Shared JS modules
├── patches/ # Patches for upstream dependencies
│ ├── chromium/ # ~159 patches to Chromium
│ ├── node/ # ~48 patches to Node.js
│ └── ... # Other targets (v8, boringssl, etc.)
├── spec/ # Test suite (1189+ TypeScript test files)
├── docs/ # API documentation and guides
├── build/ # Build configuration
├── script/ # Build and automation scripts
└── chromium_src/ # Chromium source overrides
../ # Parent directory is Chromium source
Electron uses @electron/build-tools for development. The e command is the primary CLI.
Installation:
npm i -g @electron/build-toolsConfiguration location: ~/.electron_build_tools/configs/
| Command | Purpose |
|---|---|
e init <name> --root=<path> --bootstrap testing |
Create new build config and sync |
e use <name> |
Switch to a different build configuration |
e show current |
Display active configuration name |
e show configs |
List all available configurations |
| Command | Purpose |
|---|---|
e sync |
Fetch/update all source code and apply patches |
e sync --3 |
Sync with 3-way merge (required for Chromium upgrades) |
e build |
Build Electron (runs GN + Ninja) |
e build -k 999 |
Build and continue on errors (up to 999) |
e build -t <target> |
Build specific target (e.g., electron:node_headers) |
e start |
Run the built Electron executable |
e start --version |
Verify Electron launches and print version |
e test |
Run the test suite |
e debug |
Run Electron in debugger (lldb on macOS, gdb on Linux) |
| Command | Purpose |
|---|---|
e patches <target> |
Export patches for a target (chromium, node, v8, etc.) |
e patches all |
Export all patches from all targets |
e patches --list-targets |
List available patch targets |
# 1. Ensure you're on the right config
e show current
# 2. Sync to get latest code
e sync
# 3. Make your changes in shell/ or lib/ or ../
# 4. Build
e build
# 5. Test your changes (Leave the user to do this, don't run these commands unless asked)
e start
e test
# 6. If you modified patched files in Chromium:
cd .. # Go to Chromium repo
git add <files>
git commit -m "description of change"
cd electron
e patches chromium # Export the patchElectron patches upstream dependencies (Chromium, Node.js, V8, etc.) to add features or modify behavior.
How patches work:
patches/{target}/*.patch → [e sync --3] → target repo commits
← [e patches] ←
Patch configuration: patches/config.json maps patch directories to target repos.
Key rules:
- Fix existing patches 99% of the time rather than creating new ones
- Preserve original authorship in TODO comments
- Never change TODO assignees (
TODO(name)must retain original name) - Each patch file includes commit message explaining its purpose
Creating/modifying patches:
- Make changes in the target repo (e.g.,
../for Chromium) - Create a git commit
- Run
e patches <target>to export
Fixing patch conflicts on an existing PR:
If asked to fix a patch conflict on a branch that already has an open PR, check the PR's failed Apply Patches CI run for an update-patches.patch artifact before running e sync locally. CI has already performed the 3-way merge and exported the resolved patch diff — applying it is much faster than a full local sync.
# Find the failed Apply Patches run for the PR, then fetch its artifact. The
# artifact is uploaded unarchived (name `update-patches.patch`), so
# `gh run download` rejects it as "not a valid zip"; the artifact zip endpoint
# returns the raw patch instead.
gh run list --repo electron/electron --branch <pr-branch> --workflow "Apply Patches" --limit 1
gh api repos/electron/electron/actions/runs/<run-id>/artifacts --jq '.artifacts[] | select(.name == "update-patches.patch") | .id'
gh api repos/electron/electron/actions/artifacts/<artifact-id>/zip > update-patches.patch
git am update-patches.patch
git pushIf no artifact exists (e.g. the 3-way merge itself failed), fall back to e sync --3 and resolve manually.
Test location: spec/ directory
Running tests:
e test # Run full test suiteTest frameworks: vitest (with mocha-style describe/it/before/after globals from spec/vitest/mocha-compat.ts), Chai
GN build arguments: Located in build/args/:
testing.gn- Debug/testing buildsrelease.gn- Release buildsall.gn- Common arguments for all builds
Main build file: BUILD.gn
Feature flags: buildflags/buildflags.gni
When working on the roller/chromium/main branch to upgrade Chromium activate the "Electron Chromium Upgrade" skill.
When working on the roller/node/main branch to upgrade Node.js activate the "Electron Node.js Upgrade" skill.
PR bodies must always include a Notes: section as the last line of the body. This is a consumer-facing release note for Electron app developers — describe the user-visible fix or change, not internal implementation details. Use Notes: none if there is no user-facing change.
When the user has write access to electron/electron, add these labels when creating PRs:
Semver label — one of:
semver/none— build changes, refactors, CI, or anything with no end-user impactsemver/patch— backwards-compatible bug fixessemver/minor— backwards-compatible new functionalitysemver/major— incompatible API changes
Backport target labels — add target/{N}-x-y for each supported release branch the change should land on. Default policy:
- Bug fixes — backport to all active release lines except the oldest
- Security fixes — backport to all active release lines including the oldest
- Features (semver/minor) and breaking changes (semver/major) — no backport labels; main-only by default
To find which release branches are active, check label colors — active target/* labels use color #ad244f, older/EOL ones use #ededed:
gh label list --repo electron/electron --search target/ --json name,color --jq '.[] | select(.color == "ad244f") | .name'C++: Follows Chromium style, enforced by clang-format
TypeScript/JavaScript: oxlint configuration in .oxlintrc.json
- Treat prefinalizers as an absolute last resort. They add GC cost to every
collection involving the object; first redesign ownership or make teardown
release only non GC resources in the destructor. Use
CPPGC_USING_PRE_FINALIZERonly when teardown unavoidably needs to inspect another GC managed object. - Destructors must not access other GC managed objects: sweeping has no object destruction order or timing guarantee.
- Never store a bare pointer or
raw_ptrto a GC managed object in a heap field. Usecppgc::Member/WeakMemberfor on heap edges andcppgc::Persistent/WeakPersistentfor off heap edges. A bare pointer is safe only as a native stack local. - Allocate GC-managed objects only with
cppgc::MakeGarbageCollected; never usenew,delete,std::unique_ptr, orscoped_refptr. - Trace every
Member,WeakMember,v8::TracedReference, and traceable helper such asgin::WeakCellFactory; call the GC managed base class'sTrace()first. Missing an edge can cause use-after-free. - Use
v8::TracedReferencefor V8 values that participate in unified heap tracing. Usev8::Globalonly for an intentionally independent V8 root. - Keep handle construction, access, and destruction on the owning isolate's thread. Use a cross thread handle only when a cross thread reference is unavoidable.
- Use
gin_helper::SelfKeepAliveonly when native asynchronous work must keep the object alive after all JavaScript references are gone. Clear it on every terminal path. - For callbacks that may outlive or escape the object, use
WeakCellFactory. Usebase::Unretained(this)only when the callback is registered on a member owned by the object and destroying that member guarantees cancellation. GetHumanReadableName()must return stable storage (normally a string literal) and must not allocate GC objects or mutate the heap.- Annotate intentionally untraced owned members that the Blink GC plugin cannot
understand, such as Mojo remotes and receivers, with
GC_PLUGIN_IGNOREand a specific justification. - Put native observer, client, and delegate registrations, native resources,
and independent V8 roots such as pending promises on a native peer derived
from
NativePeer<Wrapper>(shell/browser/native_peer.h), not on the GC wrapper. The wrapper holds the peer withNativePeerBase::Deleter, so peer teardown never runs during sweeping; keep peer destructors inert and do all teardown inTearDownNative(), reached only throughRelease(). - Native peers reach the wrapper only through
wrapper(), which returns a stack only borrow that roots the wrapper for the rest of the frame, including across JavaScript and nested run loops. Bind it withgin::WrapPersistentonly when it must escape the frame. A non-empty borrow proves only reachability, not native liveness. Refuse new native work unlessis_active().
Linting:
npm run lint # Run all linters
npm run lint:js # Run oxlint over all JS/TS/MJS sources
npm run lint:clang-format # C++ formatting
npm run lint:api-history # Validate API history YAML blocks in docs| File | Purpose |
|---|---|
BUILD.gn |
Main GN build configuration |
DEPS |
Dependency versions and checkout paths |
patches/config.json |
Patch target configuration |
filenames.gni |
Source file lists by platform |
package.json |
Node.js dependencies and scripts |
| Variable | Purpose |
|---|---|
GN_EXTRA_ARGS |
Additional GN arguments (useful in CI) |
ELECTRON_RUN_AS_NODE=1 |
Run Electron as Node.js |
# Find CL that changed a file
cd ..
git log --oneline -10 -- {file}
git blame -L {start},{end} -- {file}
# Look for Chromium CL reference in commit
git log -1 {commit_sha} # Find "Reviewed-on:" line
# Find which patch affects a file
grep -l "filename.cc" patches/chromium/*.patchGitHub Actions workflows in .github/workflows/:
build.yml- Main build workflowpipeline-electron-lint.yml- Lintingpipeline-segment-electron-test.yml- Testing
These workflows upload their findings as a build artifact named audit-results.md because GitHub Actions step summaries cannot be read via the API:
.github/workflows/audit-branch-ci.yml- Table of release-branch CI runs that errored.github/workflows/archaeologist-dig.yml- Theelectron.d.tsdiff report ("Changes Detected" patch, or a no-changes note)
The artifact is uploaded unarchived (archive: false), so it downloads as raw markdown — no unzipping needed. Agents should list the run's artifacts (GET /repos/electron/electron/actions/runs/{run_id}/artifacts), find the one named audit-results.md, and fetch its archive_download_url to read the findings directly.
Patch conflict during sync:
- Use
e sync --3for 3-way merge - Check if file was renamed/moved upstream
- Verify patch is still needed
Build error in patched file:
- Find the patch:
grep -l "filename" patches/chromium/*.patch - Match existing patch style (#if 0 guards, BUILDFLAG conditionals, etc.)
Remote build issues:
- Try
e build --no-remoteto build locally - Check reclient/siso configuration in your build config