
रूटलेस पॉडमैन कंटेनर में फ़ायरफ़ॉक्स चलाएँ, जिसमें कैपेबिलिटीज़ हटाई गई हों, नेटवर्किंग आइसोलेटेड हो और स्टोरेज एफ़ेमरल हो, ताकि सैंडबॉक्स एस्केप को सीमित किया जा सके और होस्ट से छेड़छाड़ को रोका जा सके।
# <img src="https://assets.kitploit.com/production/public/readmes/8188/0df41fe24a1ebc1de3bc0a0714b24bfb19f75a49b05ebb719cd96ff9f4f3bf5d.png" width="32" height="32" alt="foxcage icon"> foxcage
Run Firefox in a rootless Podman container for security isolation. Your browser runs with almost no Linux capabilities, in its own user and network namespace, isolated from the host — while still having full GPU acceleration, audio, and DRM support.
## Why foxcage?
Firefox already has a multi-process sandbox that isolates web content renderers using Linux namespaces and seccomp-bpf. For most threats, this is effective. foxcage adds a second wall: if an attacker exploits a vulnerability that escapes Firefox's sandbox (which happens — there are CVEs for this), they land inside a locked-down container instead of your full user session.
### What foxcage protects against
- **Post-exploit file access.** A sandbox escape on bare Firefox gives access to everything your user can read: `~/.ssh`, `~/.gnupg`, browser profiles for other browsers, password manager databases, documents, source code. In foxcage, the attacker sees only what you've explicitly mounted in.
- **On-disk tracking residue.** The `@tmp` ephemeral cage leaves zero trace on disk after the window closes — including extensions, HSTS state, TLS session cache, and DNS cache that Firefox's Private Browsing still persists. Multiple `@tmp` cages run concurrently without interfering with each other.
- **Persistence.** On bare Firefox, malware can write to `~/.config/autostart`, `~/.bashrc`, cron, or anywhere else to survive a reboot. foxcage's ephemeral container (`--rm`) means nothing persists unless you've bind-mounted it.
- **Lateral network movement.** By default, the container can't probe services on `localhost`. On bare Firefox, a sandbox escape has full network access. (Use `[network] mode = "host"` if a cage needs localhost access, e.g. for local development — but see the caveat under "Networking": host mode also exposes the host's abstract Unix sockets.)
- **Privilege escalation.** The container drops all Linux capabilities except `CAP_SYS_CHROOT` and blocks new privilege acquisition. Setuid binaries, kernel exploits via obscure syscalls, and similar escalation paths are cut off.
### What foxcage does not protect against
- **Browser-level attacks.** Phishing, malicious extensions, and anything that operates within Firefox's normal functionality is unaffected — foxcage isolates the container from the host, not the user from the browser.
- **Bind-mounted directories.** Anything you mount in (`profile`, `downloads_dir`, extra bind mounts) is fully accessible to a compromised browser. If you mount a host profile directory, an attacker can tamper with it just as on bare Firefox.
- **Audio capture via PulseAudio.** The PulseAudio socket is bind-mounted into the container. Although it is mounted read-only at the filesystem level, Unix domain sockets are bidirectional — a compromised process can still send recording requests through the socket. A browser sandbox escape could potentially record audio from the host microphone.
- **Wayland compositor exploits.** The Wayland socket is passed through. Wayland compositors isolate clients from each other by design, but a vulnerability in the compositor itself would be reachable.
### Security configuration
The container runs with:
- All Linux capabilities dropped (only `CAP_SYS_CHROOT` added back for Firefox's content sandbox; `CAP_SETUID`/`CAP_SETGID` added temporarily when `init.root` is configured)
- `no-new-privileges` to prevent privilege escalation
- Rootless user namespace (`--userns keep-id`)
- Private `/dev/shm` (not shared with host) — configurable size via `shm_size`
- Isolated networking via pasta with host loopback blocked by default
- DNS uses host DNS by default (configurable via `network.dns`)
- Only specific sockets from `XDG_RUNTIME_DIR` are bind-mounted in (Wayland, PulseAudio, PipeWire, and the filtered D-Bus proxy) — the full host runtime directory is never exposed
- Access to the host's D-Bus session bus is always mediated by a filtered `xdg-dbus-proxy` running on the host. Only `org.freedesktop.Notifications`, `org.freedesktop.portal.Desktop`, `org.mozilla.*`, and (for forks) the fork's own namespace (e.g. `org.librewolf.*`) are reachable — session services like the keyring and the SSH/GPG agent are blocked
- **Portal access is broad.** `org.freedesktop.portal.Desktop` is allowed as a whole, because that is how the file picker, "open link in another app", and screen sharing work. It also exposes `RemoteDesktop` (synthetic keyboard/mouse for the whole session), `Camera` and `Location`. Those are gated by your desktop's own approval dialogs rather than by foxcage — and the `RemoteDesktop` prompt resembles the screen-share prompt, so read approval dialogs before accepting them. `xdg-dbus-proxy` has no "deny one interface" rule, so narrowing this means enumerating every interface Firefox needs; see `docs/DESIGN.md` for why that is not done by default
- All bind mounts (`profile`, `downloads_dir`, extra `[mounts] bind`) use `nosuid,noexec`
- Browser download verified against GPG signatures: Firefox against Mozilla's signed SHA-512 checksums, LibreWolf against the LibreWolf Maintainers detached signature plus sibling SHA-256. Verification is stricter than `gpg --verify`, which exits 0 for a signature made by a *revoked* key and for any key in the keyring. foxcage additionally requires that the signature chains to the pinned primary key, and refuses any release signed by a subkey its owner revoked as compromised — see [Revoked signing keys](#revoked-signing-keys)
- Ephemeral container (`--rm`) — filesystem writes are lost on exit
- No host devices (webcam, security keys, printers) passed through unless explicitly enabled
Each `[network]` and `[mounts]` option you enable trades some isolation for convenience. The defaults are the most restrictive configuration that still gives you a usable browser.
## Requirements
- Python 3.11+
- Podman (rootless)
- Wayland compositor (X11 is not supported)
- pasta (`sudo apt install passt`) — unless `network.mode = "host"`
- xdg-dbus-proxy (`sudo apt install xdg-dbus-proxy`)
- PulseAudio or PipeWire with PulseAudio compatibility (for audio)
- GPU with DRI support — optional; without `/dev/dri` foxcage warns and Firefox renders in software. VA-API drivers for Intel, AMD and nouveau are installed in the image, so hardware video decoding works without host driver packages — see [Hardware video decoding](#hardware-video-decoding-va-api)
Run foxcage as your normal desktop user, not as root or via `sudo` — the sandbox maps your user into the container, and running as root removes the isolation foxcage exists to provide. It refuses to start as root.
**Tested environment:** Debian 13 (Trixie) with GNOME 3. Other Linux distributions and Wayland compositors may work but have not been tested.
## Installation
foxcage is a single Python script with no dependencies outside the Python standard library. Copy it to a directory in your `PATH`:
```sh
sudo cp foxcage /usr/local/bin/foxcage
```
Or for a user-local install:
```sh
cp foxcage ~/.local/bin/foxcage
```
Make sure the script is executable (`chmod +x foxcage`).
Check which revision you have with `foxcage --version` — useful when reporting a problem, since foxcage is installed by copying a single file.
## Usage
```sh
./foxcage
```
On first run the script builds the container image (downloads Firefox from Mozilla, installs minimal Debian dependencies) and then starts Firefox. On subsequent runs, foxcage checks for Firefox updates and rebuilds the image automatically when a new version is available. The image is also rebuilt periodically (every 7 days by default) to pick up system package updates. If the update check fails (network error, timeout), a warning is logged and the existing image is used — startup is never blocked.
Pass arguments through to Firefox:
```sh
./foxcage https://example.com
```
Combine a named cage with Firefox flags:
```sh
./foxcage @work --kiosk https://example.com
```
If a cage is already running, the URL opens in a new tab in the existing browser instead of starting a second container. Running `foxcage` (or `foxcage @cage`) with no URL against a running cage exits cleanly with a "cage is already running" message — foxcage can't raise an existing Wayland window from outside the container, so it doesn't try.
Per-launch flags do **not** apply when a cage is already running. `--dns`, `--ipv4-only`, `--lifetime`, `--color` and `--fork` are consumed when the container starts, and a running container's settings can't be changed from outside, so they are ignored with a warning. Close the cage and re-run to apply them.
> Use the `private_browsing` config key for private-mode sessions — *not* Firefox's raw `--private-window` CLI flag. The config key sets session-wide private mode (`browser.privatebrowsing.autostart`), so subsequent `foxcage @cage URL` invocations can reopen in tabs. `--private-window` as a Firefox passthrough would make only the first window private and break the reopen-in-tab behaviour above.
>
> **Heads up:** sessions enabled via `private_browsing = true` don't show Firefox's usual private-window UI cues (purple accent bar, mask icon, "(Private Browsing)" in the title). That's because every window in the session is private, so Firefox has no non-private window to visually contrast against — it suppresses the indicator. The session *is* genuinely private; verify if you want by visiting `about:privatebrowsing` in the cage (shows the standard Private Browsing info page) or `about:config` and checking `browser.privatebrowsing.autostart = true`.
### Ephemeral browsing with `@tmp`
For one-off links that should leave no trace, use the reserved `tmp` cage:
```sh
./foxcage @tmp https://somewhere-suspicious.example
```
Every `@tmp` launch is a fresh, throwaway Firefox with no persistent profile. When the window closes, **everything** is gone — cookies, cache, history, extensions, HSTS state, TLS session cache, DNS cache, saved tab state. This goes further than Firefox Private Browsing, which still persists extensions and a fair bit of on-disk state.
Multiple `@tmp` cages run concurrently, each isolated from the others. The menu bar shows `FoxCage - tmp (<short id>)` so you can tell concurrent ephemeral windows apart.
Ephemeral cages open a blank page on startup and blank new tabs — the default Firefox home page and new-tab content (top sites, Pocket recommendations, activity stream) are pure noise on a fresh profile that's about to be thrown away, so they're suppressed. Persistent cages keep Firefox's defaults.
#### Named ephemeral cages
If you want a meaningful name on a throwaway session (say, a research rabbit hole you'll want to reopen in a new tab), use `@tmp-<name>`:
```sh
./foxcage @tmp-research https://example.com # first call → new window
./foxcage @tmp-research https://another.example # second call → new tab in the existing window
```
`@tmp-<name>` is still ephemeral — when you close the window, everything goes. The difference from bare `@tmp` is that second launches with the same name **reuse the existing window** (same as persistent cages), so you can add more tabs later without starting a parallel copy. Bare `@tmp` keeps its "every launch a fresh throwaway" behaviour.
The menu-bar label shows the name you picked (`FoxCage - tmp-research`) so the window is labelled meaningfully.
#### Customising ephemeral defaults
Create `~/.config/foxcage/tmp.toml` to set defaults for all ephemeral cages (both bare `@tmp` and every `@tmp-<name>`). For example:
```toml
private_browsing = true
lifetime = "30m"
extensions = ["ublock-origin"]
[network]
dns = "cloudflare"
```
Every ephemeral launch now gets a private window, uBlock Origin, Cloudflare DoH, and auto-closes after 30 minutes — with full ephemerality intact. Named ephemerals inherit `tmp.toml` by default; if you want to override per name, create `~/.config/foxcage/tmp-<name>.toml`. That file then applies instead of `tmp.toml` — no merging, the more-specific file wins outright. Copy shared defaults into it if you want them.
Anything you can set in a regular cage's config works here, *except* the one key that would defeat ephemerality itself:
- `profile` — hard error.
It points at a persistent on-host profile directory, which directly contradicts what `@tmp` is for. If you want a sandboxed cage with a persistent profile, use a regular named cage (`@work`, `@research`, etc.) that doesn't start with `tmp-`.
### Pre-installed extensions
A cage can have extensions baked into its image and force-installed on every launch:
```toml
extensions = ["ublock-origin"]
```
Put that in `~/.config/foxcage/tmp.toml` and **every throwaway cage starts with uBlock Origin already running** — which matters, because an ephemeral cage is otherwise the least protected browser you have, used on exactly the links you trust least. A fresh `@tmp` profile has no extensions at all, and installing one by hand is pointless in a session that shreds itself on close.
Each entry can be the short name from the add-on's addons.mozilla.org URL, the listing URL itself, or the add-on ID:
```toml
extensions = [
"ublock-origin",
"https://addons.mozilla.org/firefox/addon/noscript/",
"[email protected]",
]
```
Add-ons are resolved through the addons.mozilla.org API at launch — one lookup per add-on, run concurrently with each other and with the browser version check, and cached for three hours like it — then downloaded during the image build and verified against the SHA-256 AMO publishes. The resolved version is part of the Containerfile, so a new release of an extension changes the image hash and triggers a rebuild — extensions update the same way Firefox does, and for the same reason: nothing is installed at runtime, so a fresh ephemeral profile never re-downloads anything.
Because they are installed by enterprise policy rather than by hand:
- They cannot be removed or disabled from inside the cage (`about:addons` shows them as installed by your organisation).
- They are enabled in private windows, so they still work in a cage running `private_browsing = true`. This needs Firefox 136 or ESR 128.8; older versions ignore the setting and leave the add-on inert in private windows.
- Their icon is placed in the toolbar, so you can see the blocker is actually there.
- In-browser extension updates are off. Updates arrive with image rebuilds.
Limitations:
- **AMO only.** Extensions must be listed on addons.mozilla.org — that lookup is what supplies the add-on ID a policy entry needs, plus the digest the build verifies. A bare `.xpi` URL is rejected.
- **No version pinning.** A rebuild takes whatever AMO currently lists as the add-on's latest version, or the cached answer if one is less than three hours old. `--rebuild` always re-checks.
- **AMO must be reachable to build.** If the lookup fails and the cage already has an image, foxcage warns and launches with the existing one; if the cage has no image yet, it exits rather than build a cage silently missing the add-ons you asked for.
- **All ephemeral cages share one image.** `@tmp` and every `@tmp-<name>` build a single `foxcage-tmp` image, so giving one named ephemeral a different `extensions` list than `tmp.toml` makes the two rebuild over each other on alternate launches. Keep ephemeral extension lists in `tmp.toml`.
### Overriding DNS per launch
The `--dns` flag (and the equivalent `network.dns` config key) accepts three forms:
```sh
./foxcage @tmp --dns 1.1.1.1 https://example.com # IP
./foxcage @tmp --dns cloudflare https://example.com # alias
./foxcage @tmp --dns https://dns.nextdns.io/<id> # custom DoH URI
```
**When the value matches a known provider (by alias or by IP), foxcage automatically enables forced DNS over HTTPS to that provider.** Firefox's TRR gets set to mode 3 (strict, no cleartext fallback) with the bootstrap address filled in so there's no unencrypted resolution leak at startup. You see a one-line notice on stderr like `Enabling DNS over HTTPS via Cloudflare`.
Built-in aliases:
| Alias | IP | Filtering |
|-------|------|-----------|
| `cloudflare` | 1.1.1.1 | none |
| `cloudflare-security` | 1.1.1.2 | blocks malware |
| `cloudflare-family` | 1.1.1.3 | blocks malware + adult |
| `google` | 8.8.8.8 | none |
| `quad9` | 9.9.9.9 | blocks malware (Quad9 default) |
| `quad9-unfiltered` | 9.9.9.10 | none |
| `adguard` | 94.140.14.14 | blocks ads + trackers |
| `adguard-family` | 94.140.14.15 | ads + trackers + adult |
| `opendns` | 208.67.222.222 | some |
An IP that isn't in the table (e.g. your LAN's Pi-hole) stays cleartext only — no DoH is enabled, since foxcage doesn't know the corresponding DoH endpoint. Use the URI form for that: `--dns https://pi.hole/dns-query` (with a valid cert) enables DoH and leaves container DNS alone.
The URI form skips setting the container's cleartext DNS, so anything inside the container that isn't Firefox still uses the host's DNS. This is deliberate — `--dns URI` means "make Firefox use this DoH resolver", full stop.
`--dns` is incompatible with `network.mode = "host"`, which already has full host network access.
### Visual cage identification
Every named cage gets an accent colour in the menu bar so you can tell windows apart at a glance. **You don't need to configure anything** — the colour is deterministically derived from the cage name (SHA256-hashed into a hue, with fixed saturation and lightness). `@banking`, `@work`, `@personal`, `@tmp-research` all get distinct, stable colours without you lifting a finger.
The default (anonymous) cage keeps the baked-in orange.
If you want to override the auto-derived colour, set it explicitly:
```toml
# ~/.config/foxcage/banking.toml
color = "#dc2626" # red — overrides the auto-derived colour
```
```sh
./foxcage @experiment --color "#10b981" https://example.com # teal, one-off
```
Accepts standard CSS hex: `#rgb`, `#rrggbb`, or `#rrggbbaa` (with alpha). The auto-derived colours are tuned to be visible on both light and dark menubars (lightness fixed at 55%, saturation at 75%), so you shouldn't need to override for theme reasons.
### Time-limited cages
The `--lifetime` flag (and equivalent `lifetime` config key) auto-closes a cage after a set duration. Format is `<number><unit>` with unit `s`, `m`, or `h`:
```sh
./foxcage @tmp --lifetime 10m https://example.com
./foxcage @work --lifetime 2h
```
The countdown starts when Firefox actually launches inside the cage — container startup and image-build time don't eat into your budget. The cage's menu-bar label shows the countdown alongside the cage identity — e.g. `FoxCage - tmp (a3f2b1) | 9m` — updated once per minute while there's more than a minute left, and once per second in the final minute. When the countdown hits zero Firefox closes itself and the container exits. If you close Firefox yourself before the lifetime is up, nothing unusual happens.
Set a default lifetime per cage in its config:
```toml
# ~/.config/foxcage/tmp.toml — every @tmp launch auto-closes after 15 minutes
lifetime = "15m"
private_browsing = true
```
`--lifetime` on the command line wins over any config value.
Force a full image rebuild (re-downloads Firefox and all system packages):
```sh
./foxcage --rebuild
```
A running container keeps whichever image it was started from, even after foxcage rebuilds the image tag. If you try to open a tab in a cage whose image has since been updated (by `--rebuild`, a Firefox update, or the scheduled rebuild), foxcage refuses with an error (also surfaced as a desktop notification) and asks you to quit Firefox and re-launch — which starts a fresh container on the current image. Under `--rebuild` with a live cage, foxcage warns up front, does the build, then applies the same check.
### Updates
foxcage checks for new browser releases — Mozilla's release API for Firefox, GitLab's releases endpoint for LibreWolf. If an update is available, the container image is rebuilt automatically. The image is also rebuilt periodically (every 7 days by default) to pick up Debian security updates. The browser's built-in auto-updater is disabled since updates are handled at the image level.
If the update check fails (no network, API timeout), a warning is printed and the existing image is used — you can always browse.
**Answers are cached for three hours**, in `~/.cache/foxcage/lookups.json` (or `$XDG_CACHE_HOME`), so a launch inside that window makes no network call at all and starts straight away. Extension lookups are cached on the same terms. `--rebuild` ignores the cache and goes and looks, so there is nothing to clear by hand.
When lookups *do* happen they run concurrently — the browser version check and every extension together — so a launch waits for the slowest one rather than for all of them added up. Each prints a line naming the host it is about to wait on, because these calls run before anything else foxcage prints:
```
Checking product-details.mozilla.org for the latest Firefox release version...
Looking up extension 'ublock-origin' on addons.mozilla.org...
Enabling DNS over HTTPS via Cloudflare
Detected Wayland (wayland-0)
Starting Firefox...
```
Paths that need no request stay silent: a cache hit, and a fully-qualified pin like `version = "149.0.2"`, are used as-is and cost nothing.
**If a launch pauses for tens of seconds on one of those lines, suspect DNS.** The HTTP timeout covers the connection, not hostname resolution, so a stale or unreachable nameserver — routine in the seconds after a VPN comes up — blocks before the request is even sent. `getent ahosts product-details.mozilla.org` reproduces the same delay on its own.
A *blackholed IPv6 route* used to cause the same symptom and no longer does. Some VPNs leave IPv6 configured but drop the traffic, and Python's standard connect tries every AAAA record to exhaustion before the first A record — 40 seconds against addons.mozilla.org. foxcage races the address families instead (RFC 8305, the same "Happy Eyeballs" your browser does), so a dead family costs about 250 ms rather than the timeout.
Update cadence lives at the top level of the config; version and channel pinning live in the per-fork section:
```toml
rebuild_days = 14 # rebuild for base-image updates every 14 days (0 to disable)
[firefox]
channel = "beta" # track the beta channel instead of stable (firefox only)
version = "149" # pin to Firefox 149.x (latest patch release)
```
**Pinning an ESR version needs the channel too.** Mozilla's version index lists ESR releases without the `esr` suffix their downloads carry, so a bare `version = "140"` on the default channel resolves to a release that doesn't exist. Set both:
```toml
[firefox]
channel = "esr"
version = "140" # → 140.13.0esr
```
A pin that matches no release is now an error naming the pin, rather than quietly falling back to the latest release. A temporary failure to reach Mozilla's API still warns and carries on with the existing image, so a flaky network never blocks startup.
Suffixed pins must be fully qualified — `"140.13.0esr"` and `"150.0b9"` work, `"140esr"` and `"150b9"` are rejected at config load because no release can ever match them. The same applies to LibreWolf revisions: `"146.0.1-1"` works, `"146-1"` does not.
To force an immediate full rebuild: `./foxcage --rebuild`
### Revoked signing keys
foxcage refuses to install a browser build whose signature was made by a signing subkey the upstream project revoked as **compromised** (RFC 4880 revocation reason `0x02`). `gpg --verify` does not do this on its own: it prints a warning and exits 0, so without the extra check a leaked signing key would still authenticate a tampered download.
A refusal looks like this, and fails the build rather than installing:
```
foxcage: REFUSING /tmp/SHA512SUMS - signed by 09BEED63F3462A2DFFAB3B875ECB6497C1A20256,
which its owner revoked as compromised. This build cannot be trusted; wait for
upstream to re-sign this release with a current key.
```
There is nothing to configure and no override. If you hit this, the fix is upstream's: either pin a release signed with a current key, or wait for the affected release to be re-signed.
Routine key rotation is treated differently. A subkey revoked as superseded, retired, or with no reason given does not invalidate signatures made *before* the revocation, so those releases install with a warning. A signature dated *after* any revocation is refused whatever the stated reason.
**Mozilla's August 2026 key rotation.** Mozilla revoked the signing subkey `09BEED63…C1A20256` on 2026-08-06 after an unencrypted copy was committed to a private GitHub repository, and replaced it with `827E6586…76767AA3`. Firefox releases signed with the old subkey — everything between 2025-03-13 and 2026-08-06, which at the time of writing still includes the current ESR (`140.13.0esr`) and any `version` pin into that window — are refused by the check above. Release and beta channels are unaffected. foxcage fetches keys from `keys.openpgp.org` rather than `keyserver.ubuntu.com` because the latter served neither the replacement subkey nor the revocation for days after the rotation; a stale keyserver would both break builds outright and silently reduce the revocation check to a no-op.
### Firefox forks (LibreWolf)
foxcage can run a privacy-oriented Firefox fork in place of upstream Firefox:
```toml
fork = "librewolf" # default is "firefox"
[librewolf]
version = "146.0.1-1" # optional pin; partial pins ("146", "146.0.1") also work
```
Or per-launch via CLI:
```sh
foxcage @tmp --fork librewolf https://example.com
```
**LibreWolf**: privacy-hardened Firefox fork — strict tracking protection, DoH, RFP, telemetry locked off by default. Signed Linux tarball from GitLab (`librewolf-community/browser/bsys6`), GPG-verified against the LibreWolf Maintainers key `662E 3CDD 6FE3 2900 2D0C A5BB 4033 9DD8 2B12 EF16` with a sibling `.sha256sum` cross-check, under the same [revocation rules](#revoked-signing-keys) as Firefox. LibreWolf's bundled `librewolf.cfg` is preserved; foxcage appends its own prefs on top rather than overwriting. The maintainers rotated their signing subkey on 2026-04-25 without stating a reason; current tarballs were signed before that date, so they install with a warning rather than being refused.
**Channel is Firefox-only**: `firefox.channel = "beta" | "esr"` is rejected when `fork` is anything other than `"firefox"`. LibreWolf has a single release track.
Switching `fork` (via config or `--fork`) changes the Containerfile hash, which triggers a rebuild on the next launch — no manual `--rebuild` needed.
#### Profile compatibility
> **Use a dedicated profile per fork.** The safest default is to let foxcage provision its own profile (omit `profile` from config), or point `profile` at a directory you don't also open from the host.
- **LibreWolf**: *usually* fine to share with your host Firefox profile — LibreWolf tracks Firefox versions within a few days, so `compatibility.ini` schema conflicts are rare. Risks: (1) only sequential use is safe (Firefox's lock file prevents concurrent opens); (2) in the short window after a Firefox stable release, running Firefox first then LibreWolf may trigger a "used by a newer version" migration dialog; (3) features LibreWolf strips (Sync, Pocket, Mozilla account) silently don't work but don't corrupt data.
### Named cages
Run separate sandboxed instances with their own config and Firefox profile:
```sh
./foxcage @work
```
This loads `~/.config/foxcage/work.toml` and uses a separate image (`foxcage-work`), container (`foxcage-work`), and volume (`foxcage-work-profile`). The config file must exist for named cages. Cage names may only contain letters, digits, hyphens, and underscores.
## Configuration
Config files live in `$XDG_CONFIG_HOME/foxcage/` (defaults to `~/.config/foxcage/`).
- `config.toml` — default cage (optional, sensible defaults without it)
- `<name>.toml` — named cage, loaded with `@<name>` (required)
Unknown config keys are rejected with an error. See `config.toml.example` for all available options with defaults.
### Example config.toml
```toml
# Bind-mount a host Firefox profile directory into the cage
profile = "~/.mozilla/firefox/xxxxxxxx.default-release"
# Allow downloading files to ~/Downloads
downloads_dir = "~/Downloads"
# Extensions pre-installed into the cage (addons.mozilla.org short name,
# listing URL, or add-on ID). Most useful in tmp.toml.
# extensions = ["ublock-origin"]
# Shared memory size for Firefox IPC (default: 256m)
# shm_size = "256m"
# Pass through webcam devices (/dev/video*)
# webcam = true
# Pass through host CUPS socket for locally-connected printers (e.g. USB)
# local_printers = true
# Pass through FIDO2/U2F security key devices (/dev/hidraw*)
# security_keys = true
# Always open Firefox in private browsing mode
# private_browsing = true
# Auto-close the cage after a duration (<int> with unit s, m, or h)
# lifetime = "30m"
# Accent colour for the menu-bar label. Named cages get a colour derived
# from the name automatically; set this to override it.
# color = "#4a90e2"
# Browser fork: "firefox" (default) or "librewolf"
# fork = "librewolf"
# Full image rebuild interval in days for base-image updates (default: 7, 0 to disable)
# rebuild_days = 7
[firefox]
# Firefox release channel: "release" (default), "beta", "esr".
# Only valid when fork = "firefox".
# channel = "release"
# Pin to a specific Firefox version (overrides channel).
# Partial versions like "149" or "149.0" resolve to the latest patch release.
# Suffixed versions must be fully qualified ("140.13.0esr", "150.0b9"); to
# follow the ESR line by major version, pair a numeric pin with
# channel = "esr" above.
# version = "149.0.2"
[librewolf]
# Pin to a specific LibreWolf version. Tags are "<firefox-version>-<rev>",
# e.g. "146.0.1-1". Partial pins like "146" or "146.0.1" also work.
# version = "146.0.1-1"
[network]
# "host" for full host networking (needed if the cage has to reach services
# on the host's localhost), or omit for isolated pasta (default)
# mode = "host"
# DNS server (isolated mode only, default: host DNS)
# dns = "1.1.1.1"
# Disable IPv6 in the cage (isolated mode only)
# ipv4_only = true
[mounts]
# Additional bind mounts into the container. Supported forms:
# "~/Documents" — same path in container
# "~/Documents:~/Documents" — ~ expanded on both sides
# "~/Documents:/home/user/Documents" — explicit container path
# Append :ro for read-only, e.g. "~/Documents:ro"
# nosuid,noexec are always enforced on bind mounts; an explicit "exec" or
# "suid" is rejected rather than silently dropped.
# Host paths must be absolute or start with "~/".
bind = [
"~/Documents:ro",
]
[init]
# Commands to run at image build time (as root). Changes trigger a rebuild.
# build = ["apt-get update && apt-get install -y --no-install-recommends vim"]
# Commands to run at container startup as root, before Firefox.
# root = ["chown user:user /some/path"]
# Commands to run at container startup as your user, before Firefox.
# user = ["mkdir -p ~/custom-dir"]
```
### Host Firefox profile
To share a host Firefox profile with the cage, set `profile` to the profile directory. Find your profile path by visiting `about:profiles` in Firefox on the host — or just point at a fresh empty directory if you want the cage to start with a clean profile that persists on the host.
```toml
profile = "~/.mozilla/firefox/xxxxxxxx.default-release"
```
Only this one directory is bind-mounted into the cage. Sibling profiles under `~/.mozilla/firefox/` and the `profiles.ini` registry are not exposed — a compromised cage cannot tamper with them.
If `profile` is unset, a named Podman volume stores the Firefox profile instead (see "What persists" below). If the same profile is already open in Firefox on the host, Firefox's per-profile lock file will cause a clash — use a dedicated profile per cage.
### Networking
By default, the container uses pasta with host loopback blocked and host DNS. pasta requires podman 4.4 or newer (it has been the rootless default since podman 5.0).
---
[Read more](https://gitlab.com/grepular/foxcage)