Build and maintain your own Firefox-based browser with a patch-first workflow
FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as a series of patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths, and build the result. It also ships Furnace, a component system for creating and overriding Firefox custom elements under toolkit/content/widgets.
Inspired by fern.js and Melon.
-
Patch-based fork management Your customisations live as portable, ordered
.patchfiles. Export single files, multiple paths, or everything at once. Contextual diffs mean upstream security fixes are not silently dropped when you rebase. -
Semi-automated ESR rebase
fireforge rebasereplays your patch stack onto new Firefox source with escalating fuzz matching. When a patch fails, you fix it manually and--continue. The full stack gets re-exported with updated version stamps. -
Wiring and registration
fireforge wireandfireforge registerinject your code into Mozilla's startup paths, build manifests, and JAR files with a single command. The injection is AST-based (via Acorn), so it survives formatting changes applied between versions. -
Furnace component system Override existing Firefox custom elements or create new ones under
toolkit/content/widgets(CSS-only restyles, full behavioural forks, or entirely new widgets). -
Design token management Track CSS custom property coverage across your modified files.
-
Quality checks
fireforge lintcatches fork-specific issues (raw colours, missing licence headers, relative imports, large patches, cross-patch ordering problems) before you export.fireforge verifyruns a read-only integrity check over the whole patch queue.fireforge doctordiagnoses project health including Furnace component validation. -
Built and validated against real Firefox code Developed by editing a real Firefox ESR codebase, learning from existing patch tools, observing the breakages and edge cases that surfaced, and turning those findings into a realistic test suite. In-repo tests are thus grounded in actual development scenarios. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Github Actions will be added soonishlyTM.
- Node.js 20+
- Python 3 (required by Firefox's
machbuild system). - Git
- Platform build tools: Xcode on macOS,
build-essentialon Linux, Visual Studio Build Tools on Windows.
mkdir mybrowser && cd mybrowser
npm init -y
npm install --save-dev @hominis/fireforge
npx fireforge setup # interactive project init
npx fireforge download # fetch Firefox source (~1 GB)
npx fireforge bootstrap # install build deps (may need sudo)
npx fireforge import # apply your patches (if any exist)
npx fireforge build # build the browser
npx fireforge run # launch itYour project now has fireforge.json, an engine/ directory with Firefox source, and a patches/ directory with an empty patches.json manifest ready for your first customisation.
# 1. Make changes inside engine/
# Edit browser/base/content/browser.js, add CSS, create new modules...
# 2. Export your changes as a patch
npx fireforge export browser/base/content/browser.js \
--name "custom-toolbar" --category ui
# 3. Your patch is now in patches/001-ui-custom-toolbar.patch
# with metadata tracked in patches/patches.json
# 4. Later, reset and replay to verify everything applies cleanly
npx fireforge reset --yes
npx fireforge import # --dry-run to preview without applying
# 5. When Firefox releases a new ESR, update fireforge.json, re-download, and rebase
npx fireforge download --force
npx fireforge rebasePatches live in patches/, applied by numeric filename prefix, and tracked in patches/patches.json:
patches/
001-branding-custom-logo.patch
002-privacy-disable-telemetry.patch
003-ui-sidebar-tweaks.patch
patches.json
Categories: branding | ui | privacy | security | infra
The category system is intentionally broad. The numeric ordering provides sequencing.
# Apply all patches from patches/ to the engine
fireforge import
# Preview what would be applied without modifying the engine
fireforge import --dry-run
# Apply patches up to (and including) a specific one
fireforge import --until 003-ui-sidebar-tweaks.patch
# Keep going if a patch fails instead of stopping
fireforge import --continue
# Force-apply even when the engine has drifted or has unmanaged changes
fireforge import --force# Single file
fireforge export browser/base/content/browser.js
# Multiple paths with metadata
fireforge export browser/modules/mybrowser/*.sys.mjs \
--name "storage-infra" --category infra
# Everything at once
fireforge export-all --name "all-changes" --category ui
# Regenerate patches after further edits
fireforge re-export --all --scan
# Preview what an export would do without writing
fireforge export browser/base/content/browser.js --dry-run
# Insert a new patch at a specific position
fireforge export browser/base/content/browser.js --order 3 --name "inserted" --category ui
fireforge export browser/base/content/browser.js --before 005-ui-sidebar.patch --name "prelim"
# Restrict a re-export to a specific file subset
fireforge re-export --files browser/base/content/browser.js 002-ui-toolbar- Update
firefox.versioninfireforge.json fireforge download --forcefireforge rebase- Fix any rejects, then
fireforge rebase --continue - If stuck,
fireforge rebase --abortto restore the pre-rebase state
When fireforge import fails on a patch, fix the .rej files in engine/, then:
fireforge resolveThis re-exports the fixed patch and continues applying the remaining stack.
Patch manifest format
patches/patches.json is updated automatically by export and re-export:
{
"version": 1,
"patches": [
{
"filename": "001-branding-custom-logo.patch",
"order": 1,
"category": "branding",
"name": "custom-logo",
"description": "Replaces default Firefox branding with custom logo",
"createdAt": "2025-01-15T10:30:00Z",
"sourceEsrVersion": "140.9.0esr",
"filesAffected": ["browser/branding/official/logo.png"]
}
]
}If the manifest drifts after an interrupted export or manual edits, fireforge import will stop rather then silently applying a stale stack. Use fireforge doctor --repair-patches-manifest to rebuild it from disk. Because the rebuild is deterministic, the result will always be consistent with what is actually on the filesystem.
Patch lint checks
fireforge lint runs automatically during export, export-all, and re-export. Use --skip-lint to downgrade errors to warnings. Errors block the export; warnings are printed but do not block.
| Check | Scope | Severity |
|---|---|---|
missing-license-header |
New files (JS/CSS/FTL) | error |
relative-import |
JS/MJS files | error |
token-prefix-violation |
CSS files (with furnace) | error |
raw-color-value |
Introduced CSS color values | error |
duplicate-new-file-creation |
Same path created by multiple patches | error |
forward-import |
Patch imports from a later-patch file | error |
missing-modification-comment |
Modified upstream JS/MJS | warning |
file-too-large |
New files >650 lines | warning |
missing-jsdoc |
Exports in new .sys.mjs |
warning |
observer-topic-naming |
Observer topics with binaryName | warning |
large-patch-files |
Patches affecting >5 files | warning |
large-patch-lines |
Patches >300 lines | warning |
The two cross-patch rules (duplicate-new-file-creation and forward-import) run over the whole patch queue rather than a single diff, catching ordering issues that only surface during import. Forward-import detection compares leaf filenames, so a false positive is theoretically possible when two patches create files with the same basename in different directories. Suppress with an inline // fireforge-ignore: forward-import comment on or above the import line. This is currently the only lint rule that supports inline suppression.
When a patch queue drifts — overlapping new-file creations, forward imports, manifest desync — start with diagnosis:
fireforge verify # fsck: manifest + cross-patch lint
fireforge lint # includes the same cross-patch rules
fireforge status --ownership # flat path → owning patch table
fireforge status --json # machine-readable classified outputThen fix with the appropriate primitive:
| Problem | Fix |
|---|---|
| Two patches each creating the same file | fireforge patch delete <duplicate> or fireforge re-export --files |
| A patch imports from a module in a later patch | fireforge patch reorder <later> --before <importer> |
| Wrong patch ordering | fireforge patch reorder <patch> --to <N> |
| A patch claims files that belong elsewhere | fireforge re-export --files <subset> <patch> |
| Manifest references a missing patch file | fireforge doctor --repair-patches-manifest |
| Unmanaged changes you want to discard | fireforge discard <file> or fireforge reset |
Every destructive command defaults to an interactive confirmation with a change summary. --dry-run previews without writing; --yes skips the prompt for CI; --force-unsafe bypasses structural refusals when you have context the linter cannot see. Do not hand-edit patches.json — it is owned by FireForge.
# Wire a subscript with init/destroy lifecycle
fireforge wire my-widget --init "MyWidget.init()" --destroy "MyWidget.destroy()"
# Register a file in the correct build manifest
fireforge register browser/modules/mybrowser/MyStore.sys.mjs
# Both support --dry-run to preview changesWire options
- Subscript (always): Adds
loadSubScriptcall tobrowser-main.js --init <expr>: Adds init expression togBrowserInit.onLoad()inbrowser-init.js--destroy <expr>: Adds destroy expression toonUnload()(LIFO ordering, which matters because destroy handlers that run in the wrong order can leave dangling references)--after <name>: Controls ordering between dependent subscripts--dom <file>: Inserts#includedirective for.inc.xhtmlintobrowser.xhtml--subscript-dir <dir>: Override the subscript directory (default:browser/base/content)
Supported register patterns
| File pattern | Manifest | Entry format |
|---|---|---|
browser/themes/shared/*.css |
browser/themes/shared/jar.inc.mn |
skin/classic/browser/{name}.css |
browser/base/content/*.{js,mjs} |
browser/base/jar.mn |
content/browser/{file} |
browser/base/content/test/*/browser.toml |
browser/base/moz.build |
"content/test/{dir}/browser.toml" |
browser/modules/mybrowser/*.sys.mjs |
browser/modules/mybrowser/moz.build |
"{name}.sys.mjs" |
toolkit/content/widgets/*/*.{mjs,css} |
toolkit/content/jar.mn |
content/global/elements/{file} |
Furnace manages Firefox custom elements (MozLitElement) under toolkit/content/widgets. You can override existing components or create new ones. Changes feed into the same patch workflow as everything else — Furnace is not a separate persistence layer.
There are three component types:
| Type | What it is | Local files |
|---|---|---|
| Stock | Engine components tracked for Storybook preview | None |
| Override | Forked copy: css-only (restyle) or full (JS + CSS) |
components/overrides/<name>/ |
| Custom | New element that does not exist in Firefox | components/custom/<name>/ |
fireforge furnace scan # discover components in the engine
fireforge furnace override moz-button -t css-only # fork with CSS-only restyle
fireforge furnace create moz-my-widget # scaffold a new component
fireforge furnace deploy # apply to engine/ + validate
fireforge furnace status # workspace vs engine drift
fireforge furnace diff moz-button # unified diff against baselinefurnace deploy validates components before applying — errors block, warnings are advisory. fireforge build and fireforge test --build run apply automatically. Use fireforge doctor --repair-furnace if the engine gets out of sync.
fireforge.json at your project root:
{
"name": "MyBrowser",
"vendor": "My Company",
"appId": "org.example.mybrowser",
"binaryName": "mybrowser",
"license": "EUPL-1.2",
"firefox": {
"version": "140.9.0esr",
"product": "firefox-esr"
},
"build": { "jobs": 8 },
"wire": { "subscriptDir": "browser/components/mybrowser" }
}Planned but not yet implemented:
- Docker builds Reproducible builds using Docker containers.
- CI mode Automated setup for continuous integration pipelines.
- Update manifests Generate update server manifests for auto-updates.
- Nightly support Requires implementing
hg clonesupport via mozilla-central. Currently fireforge only downloads from the archive. - E2E Github Actions Requires either a higher tier of Github offering, an external VPS or similar, or another provider entirely. In either case, full end-to-end testing is currently run solely locally.
EUPL-1.2. Firefox source in engine/ is under MPL-2.0 and is not distributed by this repository.
During fireforge setup, you choose a licence for your project files. Options: EUPL-1.2 (default), MPL-2.0, 0BSD, GPL-2.0-or-later. Firefox-derived files from Furnace always carry MPL-2.0 headers, because that is what the upstream licence requires regardless of your project-level choice.