hifox enforces Firefox hardening from a repo, on both standard and Flatpak Firefox: prefs, policies, and profile files are deployed from source, runtime state is verified against the repo, and verify stops Firefox on drift. This document maps the deploy pipeline, verification, update detection, and webapp isolation. See README.md for usage.
- overview
- install
- generation
- deploy
- systemconfig (flatpak)
- startup
- automation
- verify
- status
- drift detection
- update detection
- webapp
- webapp behavior
- clean + purge
- debug
- signaling
repo config defines desired Firefox state. hifox deploys it,
verifies runtime state, and reports drift.
ENFORCEMENT PIPELINE
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
repo (source of truth)
│
│ global_lockprefs.cfg ─── your threat model goes here
│ webapp/<name>/prefs.cfg ─ per-app overrides
│ policies.json ────────── policy-only features
│
▼
┌─ deploy ──────────────────────────────────────────────────────────┐
│ │
│ generate ──> assemble configs ──> push to Firefox ──> enforce │
│ one command. repo to runtime. nothing manual. │
│ │
└───────────────────────────┬───────────────────────────────────────┘
│
▼
┌─ runtime ─────────────────────────────────────────────────────────┐
│ │
│ lockPref() prefs normal runtime paths, extensions, │
│ and websites cannot change │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ main │ │ discord │ │ spotify │ │ ... │ │
│ │ strict │ │ mic/cam │ │ DRM │ │ your │ │
│ │ default │ │ unlocked │ │ unlocked │ │ rules │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ ╳ ╳ ╳ ╳ │
│ no shared cookies, data, or permissions │
│ │
└───────────────────────────┬───────────────────────────────────────┘
│
▼
┌─ integrity ───────────────────────────────────────────────────────┐
│ │
│ verify drift detected ──> stop Firefox ──> notify │
│ before drift continues │
│ │
│ update new pref appears in Firefox ──> diff ──> notify │
│ detection review the diff before accepting new state │
│ │
└───────────────────────────────────────────────────────────────────┘
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
ships with a default lock set. replace it with your own threat model
if needed. the architecture stays the same.
hifox install <--flatpak|--standard>
│
├── detect Firefox (refuses if the OTHER target is also installed)
│ ┌─────────────┐ ┌────────────────────────┐
│ │ Flatpak │ │ Standard │
│ │ org.mozilla │ │ HIFOX_FIREFOX_DIR, │
│ │ │ │ /usr/lib*, /opt/firefox│
│ └──────┬──────┘ └────────┬───────────────┘
│ └──────────┬──────────┘
│ ▼
├── save target ── ~/.config/hifox/target
├── deploy
├── watch install (systemd units)
└── symlink ── ~/.local/bin/hifox
four source files, one assembled output.
webapp/shared/webapp.cfg is split at a marker - per-webapp prefs
are injected into the gap.
┌───────────────────────────┐
│ global_lockprefs.cfg │─────────────────────────┐
│ (policy-type lockPrefs) │ │
└───────────────────────────┘ │
│
┌───────────────────────────┐ │
│ webapp/shared/webapp.cfg │ │
│ ┌──────────────────────┐ │ │
│ │ profile detection │ │ <── HEAD │
│ ├┄┄ marker ┄┄┄┄┄┄┄┄┄┄┄┄┤ │ <── split │
│ │ shared behavior │ │ <── TAIL ├──> autoconfig.cfg
│ └──────────────────────┘ │ │
└───────────────────────────┘ │
▲ │
│ injected between HEAD and TAIL │
┌───────────────────────────┐ │
│ webapp/<name>/prefs.cfg │ │
│ if (profileDir=="discord")│ │
│ isWebapp = true; │ │
│ <overrides> │ │
└───────────────────────────┘ │
│
┌───────────────────────────┐ │
│ generate_pref_dump.cfg │─────────────────────────┘
└───────────────────────────┘
repo Firefox
┌─────────────────────────────┐ ┌─────────────────────┐
│ │ generate │ │
│ config sources ─────────────┼────────> │ autoconfig.cfg │
│ config/autoconfig.js ───────┼── copy > │ autoconfig.js │
│ config/policies.json ───────┼── copy > │ policies.json │
│ config/user.js ─────────────┼── copy > │ user.js │
│ config/hifox.css ───────────┼── copy > │ chrome/ (default) │
│ webapp/shared/webapp.css ───┼── copy > │ chrome/ (webapps) │
│ │ │ │
└─────────────────────────────┘ └─────────────────────┘
user.js/policies may be made immutable
with chattr +i (best-effort, sudo -n)
orchestration:
hifox_deploy
│
├── per installation:
│ ┌─────────────────────────────────────────────────────┐
│ │ subshell ── isolated failure │
│ │ │
│ │ policies ──> validate JSON ──> copy ──> lock │
│ │ autoconfig ─> generate ──> copy │
│ │ webapp profiles │
│ │ ├── register in profiles.ini (next [ProfileN]) │
│ │ ├── fix StartWithLastProfile -> 1 │
│ │ └── create dir ──> copy shared CSS │
│ │ homepage ──> hifox.css + logo (default-named) │
│ │ user.js ──> copy to ALL profiles last ──> lock each│
│ └─────────────────────────────────────────────────────┘
│
├── desktop launchers (single active target)
│ ┌─────────────────────────────────────────────────────┐
│ │ shadow ─> firefox.desktop (std) │
│ │ org.mozilla.firefox.desktop (fp) │
│ │ (matches system entry name to │
│ │ preserve the existing dock pin) │
│ │ webapps org.mozilla.firefox.<name>-web.desktop │
│ │ Exec ───> launch.sh --target <t> [...] │
│ │ icon ───> cache-bust (cksum in filename) │
│ │ prune ──> remove unexpected entries │
│ └─────────────────────────────────────────────────────┘
│
├── refresh watcher (if active ── picks up new dirs)
│
└── auto-clean ──> remove remnants (skipped on failed deploy)
lock flow:
┌────────┐ ┌────────┐ ┌────────┐
│ unlock │────>│ copy │────>│ lock │
└────────┘ └───┬────┘ └────────┘
│ ▲
└── fail? ─────┘
relock where possible
(warn if it remains writable)
file ops: user-first ──> fail? ──> sudo -n fallback
(for system dirs: /etc, /usr/lib, chattr)
flatpak Firefox runs sandboxed - configs on host /etc do not reach it.
Mozilla declares an extension point: org.mozilla.firefox.systemconfig
mounted as /app/etc/firefox inside the sandbox. hifox install --flatpak
auto-runs this step when Flatpak Builder is available; the command below is
the manual rerun.
hifox install-systemconfig
│
├── flatpak Firefox required
│
├── detect runtime ── flatpak info org.mozilla.firefox
│ (sdk version + branch, no hardcoding)
│
├── stage ── ~/.cache/hifox-build.XXXX/
│ ├── manifest.yml (org.mozilla.firefox.systemconfig)
│ └── content/
│ ├── autoconfig.cfg (generated)
│ ├── autoconfig.js (copy)
│ └── policies.json (copy)
│
├── flatpak-builder ──> --user --install --force-clean
│
└── /app/etc/firefox now mounted inside sandbox
├── autoconfig.cfg
├── defaults/pref/autoconfig.js
└── policies/policies.json
one-time per install. re-run after extension uninstall or Firefox runtime changes.
hifox deploy writes new content into the registered extension dir
without rebuild, so plain content edits do not require this command.
Firefox starts
│
▼
autoconfig.js ──── bootstrap loader
│ sandbox_enabled=false (chrome access)
▼
autoconfig.cfg
┌──────────────────────────────────────────────────────┐
│ │
│ global lockPrefs ── apply global locks │
│ │ + one-shot UI seed │
│ │ (sidebar layout, alpenglow │
│ │ theme; gated by markers │
│ │ _hifox.ui_seeded / │
│ │ _hifox.alpenglow_seeded) │
│ ▼ │
│ profile detect ─── which profile? │
│ │ │
│ ├── webapp ──> selective unlock │
│ │ + shared behavior │
│ │ (keyboard, UI, devtools) │
│ │ │
│ └── main ───> keep strict default profile │
│ │ │
│ ▼ │
│ pref dump ─────── enumerate all prefs │
│ write to profile │
│ (skip volatile timestamps) │
│ │
└──────────────────────────────────────────────────────┘
│
▼
policies.json ── policy-only features (prefs can't control)
│ runs AFTER autoconfig
▼ (can override lockPref values)
hardened
user.js is empty (canary only) - all prefs managed by lockPref in autoconfig.cfg.
pref layer order: lockPref > user_pref > pref.
policies run after autoconfig and can still override overlapping settings.
two directions: you change repo -> auto-deploy.
something changes Firefox -> auto-verify.
┌──────────────────────────────────────────────────────────┐
│ REPO WATCHER │
│ hifox-watch.path ──> hifox-deploy.service │
│ │
│ PathModified ── content edits (scripts, config) │
│ PathChanged ─── new files/dirs (webapp/) │
│ PathChanged ─── profiles.ini (FF first-launch creates │
│ default profile -> auto-redeploy) │
│ │ │
│ └──> hifox deploy ──> Firefox updated │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ INTEGRITY WATCHER │
│ │
│ hifox-verify.path │
│ deployed files (PathChanged - catches deletion): │
│ autoconfig.cfg │
│ autoconfig.js │
│ policies.json │
│ │
│ profile files (PathChanged - all profiles): │
│ generated_pref_dump.txt <── Firefox start │
│ user.js <── tamper detection │
│ │
│ 5s delay (Firefox finishing writes) │
│ │ │
│ ├── hifox-verify.timer │
│ │ 60s after boot, then every 30min │
│ │ (fallback: file deletion, profile not yet │
│ │ existing at install time) │
│ │ │
│ └──> hifox verify │
└──────────────────────────┬───────────────────────────────┘
│
┌──────┴──────┐
pass fail ──> stop Firefox + notify
profile paths: only watched if profile exists at install time.
30min timer covers the gap. deploy auto-refreshes watcher paths.
Flatpak auto-deploy is user-writable. Standard Firefox touches /etc or the
install dir, so auto-deploy needs sudo -n; otherwise run hifox deploy manually.
hifox verify
│
├── no profile yet? ──> skip installation
│
▼
wait for prefs.js (up to 15s)
│
▼
┌──────────────────────────────────────────────┐
│ pref integrity (runtime dump) │
│ │
│ single source per check: │
│ generated_pref_dump.txt - what Firefox │
│ actually loaded this session │
│ (stale dump ──> "staged, restart to apply") │
│ │
│ canary, cookieBehavior, HTTPS-only, │
│ DRM, shutdown sanitization │
│ │
│ webapp profiles: each app's prefs.cfg │
│ compared against that profile's own dump │
└───────────────────┬──────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ deploy integrity │
│ │
│ policies.json ══ diff repo copy │
│ autoconfig.js ══ diff repo copy │
│ autoconfig.cfg ══ diff generated output │
│ user.js ══ diff ALL profiles │
└───────────────────┬──────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ dump monitoring │
│ │
│ profile dump ≠ repo dump? │
│ └── yes ──> cp to repo + notify │
│ │
│ dump error? ──> fail │
└───────────────────┬──────────────────────────┘
▼
┌──────┴──────┐
pass fail ──> stop Firefox + notify + exit
pref checks: default dump + webapp dumps. user.js diff: ALL profiles.
fail -> stop Firefox -> notify -> exit (run: hifox deploy).
repo is single source of truth. byte-exact compare proves sync.
hifox status
│
└── per installation:
repo live
┌───────────────────┐ ┌───────────────────┐
│ │ cmp -s │ │
│ user.js ──────────┼───vs───>│ managed profiles │ ok/warn/fail
│ policies.json ────┼───vs───>│ policies dir │ ok/warn/fail
│ autoconfig.cfg* ──┼───vs───>│ sysconfig dir │ ok/warn/fail
│ chrome assets ────┼───vs───>│ profile chrome/ │ ok/warn/fail
│ │ │ │
└───────────────────┘ └───────────────────┘
* regenerated on the fly (not a stored copy)
protected drift detected
┌──────────────┐ ┌──────────────────┐
│ │ Firefox │ │
│ managed │ update │ prefs missing │
│ prefs │ ────────> │ policies drift │
│ locked │ tamper │ files deleted │
│ policies │ delete │ user.js tamper │
│ applied │ │ │
│ │ │ │
└──────────────┘ └──────┬───────────┘
│
┌──────┴────────────────────────────┐
│ detection layers │
│ │
│ verify.path (seconds) │
│ PathChanged: 3 deployed │
│ + 2 per profile (dump, uj) │
│ │
│ verify.timer (30min fallback) │
│ file deletion, missing profile │
└───────────────────────────────────┘
│
▼
stop Firefox + notify (critical)
user runs: hifox deploy -> restart
every pref is dumped after user.js canary is loaded. Firefox updates silently
add or change prefs - hifox diffs the full dump, catches meaningful changes,
and notifies before the new state is accepted.
(volatile prefs - timestamps, counters, settings cache - skipped for clean signal.)
Firefox writes generated_pref_dump.txt inside each profile; verify copies it
into config/generated_pref_dump.<target>.txt so standard and Flatpak baselines
never overwrite each other. fresh-profile first launch is skipped (no canary
yet); next launch dumps cleanly.
┌─────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Firefox │ │ autoconfig.cfg │ │ profile/ │
│ update │───>│ pref dump runs │────>│ generated_pref_ │
│ │ │ after canary │ │ dump.txt │
└─────────┘ └──────────────────┘ └────────┬─────────┘
│
┌────────────────────────────────┐│
│ hifox verify (auto) ││
│ ▼│
│ profile dump ≠ repo dump? <───┘│
│ │ │
│ └── yes ──> cp to repo │
│ + notify-send │
└────────────────┬────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ git diff config/generated_pref_dump.<target>.txt │
│ │
│ + browser.new.feature = true <── new pref │
│ - browser.old.setting = true [LOCKED] │
│ + browser.old.setting = false [LOCKED] <── changed │
│ │
│ review ──> lockPref new threats ──> deploy │
└──────────────────────────────────────────────────────────┘
full cycle:
update ──> restart ──> dump ──> verify ──> repo ──> notify
│
git diff <┘
│
lock + deploy
.desktop ──> launch.sh --target <t> ──> find Firefox ──> exec -P <name> <url>
│ │
├── --target pins flatpak|standard ▼
└── clean stale locks separate dock icon
(0 Firefox? -> clean)
.desktop layout (single target only — install refuses if the other target exists):
Firefox shadow firefox.desktop (standard target)
org.mozilla.firefox.desktop (flatpak target)
── filename matches the system entry it shadows so the
existing dock pin keeps working
Name=Firefox
Exec=launch.sh --target <t> %u
webapps org.mozilla.firefox.<name>-web.desktop
Name=<DisplayName>
Exec=launch.sh --target <t> --webapp <name> <url>
dock icon match chain (so the OS shows <name>, not "Firefox"):
.desktop StartupWMClass=<name>-web
│
▼
launch.sh --name <name>-web --class <name>-web
│ MOZ_APP_REMOTINGNAME=<name>-web
▼
Firefox window WM_CLASS=<name>-web
│
▼
desktop env matches StartupWMClass --> taskbar shows <name>
per-webapp extension point:
${XDG_CONFIG_HOME}/hifox/hooks/webapp/<name>
if executable, launch.sh execs it instead of the default flow.
use for custom wrappers (firejail, bwrap, extra flags, ...).
global wrapper override:
HIFOX_LAUNCHER=<cmd> env var that wraps the main-browser exec
(webapp paths skip this; use hooks instead).
why not tabs or Electron:
browser tab Electron hifox (profile per app)
┌──────────────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│ A B C │ │ A │ │ B │ │ C │ │ A │ │ B │ │ C │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ same cookies │ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘
│ same perms │
│ same profile │ 3 × Chromium 1 × Firefox
└──────────────┘
no hardening lockPrefs + policies
1 leak = all open full disk access least privilege
no control no control repo-controlled policy
any webpage can become a webapp - add a folder, get an isolated profile,
a menu entry, and its own dock icon. global policy applies automatically.
override specific permissions per webapp as needed (see selective unlock below).
looks and works like a native app.
isolation:
┌──────────────────────────────────────────────────────────────────────┐
│ Firefox │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ main │ │ discord │ │ spotify │ │ example │ │
│ │ │ │ │ │ │ │ │ │
│ │ strict │ │ allow: │ │ allow: │ │ allow: │ │
│ │ locks │ │ mic │ │ DRM │ │ whatever │ │
│ │ │ │ camera │ │ Widevine │ │ it needs │ │
│ │ │ │ autoplay │ │ │ │ │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ ╳ ╳ ╳ ╳ │
│ │
│ no shared cookies, data, or permissions │
└──────────────────────────────────────────────────────────────────────┘
global lockPrefs apply to every profile. webapp overrides only unlock what
a specific app needs.
selective unlock:
┌──────────┬──────────────────────────────────────────────┐
│ main │ strict default profile │
├──────────┼──────────────────────────────────────────────┤
│ discord │ autoplay, mic, camera │
├──────────┼──────────────────────────────────────────────┤
│ netflix │ DRM (Widevine) │
├──────────┼──────────────────────────────────────────────┤
│ prime │ DRM (Widevine) │
├──────────┼──────────────────────────────────────────────┤
│ spotify │ DRM (Widevine) │
├──────────┼──────────────────────────────────────────────┤
│ example │ webapp/example/prefs.cfg -> unlock what you │
│ │ need │
└──────────┴──────────────────────────────────────────────┘
webapps should feel like apps, not browsers.
three layers strip browser behavior: prefs, keyboard, and UI.
Firefox window opens (webapp profile)
│
▼
┌──────────────────────────────────────────────────────┐
│ shared behavior (webapp/shared/webapp.cfg) │
│ │
│ prefs ──> no suggestions, no tab restore, │
│ no reader mode, no tab manager │
│ │
│ pinned tab cleanup ──> unpins leftover tabs │
└──────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
│ keyboard lockdown (layout-independent via e.code) │
│ │
│ layer 1: XUL key removal │
│ disable browser shortcuts at DOM level │
│ (22 always + 8 devtools if !debug) │
│ │
│ layer 2: keydown listener │
│ catch remaining browser combos │
│ Ctrl+W: protect first tab, close others │
└──────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
│ UI (webapp/shared/webapp.css -> userChrome.css) │
│ │
│ tab bar ──> minimal chrome │
│ single: clean, no close button │
│ multi: first tab = icon only │
│ │
│ nav bar ──> reload + uBlock only │
│ URL text invisible, permissions visible │
│ │
│ context menu ──> browser-only actions hidden │
│ │
│ override: webapp/<name>/userChrome.css appended │
│ (cascade re-shows e.g. back/forward) │
└──────────────────────────────────────────────────────┘
two levels of cleanup. clean removes remnants. purge deletes profile data.
┌─────────────────────────────────────────────────────────────────┐
│ clean │ purge │
│ safe, runs after deploy │ destructive, interactive │
├─────────────────────────────────┼───────────────────────────────┤
│ telemetry, crashes, │ profile data: cookies, │
│ experiments, caches, │ history, logins, sessions, │
│ plugins, forms, sync, │ cache, certificates, │
│ suggestions, permissions │ extensions, site state │
├─────────────────────────────────┼───────────────────────────────┤
│ keeps: everything else │ keeps: user.js, chrome/, │
│ │ profiles.ini, installs.ini │
├─────────────────────────────────┼───────────────────────────────┤
│ no confirm needed │ [y/N] confirm required │
│ auto-runs at end of deploy │ manual only │
└─────────────────────────────────┴───────────────────────────────┘
hifox clean
└── for each profile: delete known remnant files
hifox purge [--flatpak|--standard]
│
├── confirm ──── [y/N] (no piped input)
├── stop Firefox
├── pause verify watcher
│
├── per profile (main + webapps):
│ delete profile data EXCEPT user.js + chrome/
│
├── external data (whitelist what to KEEP, not what to delete):
│ flatpak: delete ~/.var/app/org.mozilla.firefox/* except config/
│ standard: delete ~/.cache/mozilla/
│
├── /tmp: Browser Toolbox temp profiles
├── resume verify watcher
│
└── next: hifox deploy ──> hardening reapplied
purge works with or without hifox hardening installed.
Browser Toolbox (not F12 - Firefox's own internal devtools) is off by default.
two flags control it independently:
global_lockprefs.cfg webapp/shared/webapp.cfg
┌────────────────────┐ ┌────────────────────┐
│ debugBrowser │ │ debugWebapp │
│ │ │ │
│ Browser Toolbox │ │ Browser Toolbox │
│ in main browser │ │ + keyboard unlock │
└────────────────────┘ └────────────────────┘
set true ──> hifox deploy ──> restart ──> Ctrl+Shift+Alt+I
hifox-specific prefs and dump files create a feedback loop between
Firefox runtime and shell tools.
Firefox startup
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ autoconfig.cfg executes inside Firefox │
│ │
│ global_lockprefs.cfg │
│ lockPref("_autoconfig.loaded", true) <── chain proof │
│ setBoolPref("_hifox.ui_seeded", true) <── UI seed │
│ setBoolPref("_hifox.alpenglow_seeded", true) <── theme seed │
│ _hifox.error.ui_seed (pref) <── inner catch │
│ _hifox.error.seed_block (pref) <── outer catch │
│ │
│ webapp/shared/webapp.cfg │
│ lockPref("_autoconfig.profile", <dir>) <── active dir │
│ lockPref("_autoconfig.error", <msg>) <── JS catch │
│ │
│ generate_pref_dump.cfg │
│ generated_pref_dump.txt <── success │
│ generated_pref_dump.err <── failure │
│ _hifox.pref_dump (pref) <── summary │
│ _hifox.error.dump_setup (pref) <── observer │
│ │
│ user.js (profile load) │
│ user_pref("_user_js.canary", "hifox") <── file proof │
│ │
└──────────────────────────────────────────────────────────────────┘
│
│ writes prefs.js canary plus dump status files
▼
┌──────────────────────────────────────────────────────────────────┐
│ hifox verify (shell side) │
│ │
│ prefs.js ──> _user_js.canary == "hifox"? │
│ generated_pref_dump.err exists and has content? (dump fail) │
│ │
│ _autoconfig.loaded ── diagnostic only (not checked) │
│ _autoconfig.profile ── diagnostic only (not checked) │
│ _autoconfig.error ── diagnostic only (not checked) │
│ _hifox.ui_seeded ── diagnostic only (not checked) │
│ _hifox.alpenglow_seeded ── diagnostic only (not checked) │
│ _hifox.pref_dump ── diagnostic only (not checked) │
│ _hifox.error.* ── diagnostic only (visible in dump) │
└──────────────────────────────────────────────────────────────────┘