Sync your Zen Browser configuration across multiple machines using a Git repository as the backbone.
Written in Rust. Linux only for now.
Status: v0.1.0. Declarative, NixOS-style config: you choose exactly what syncs — sessions, cookies and history are off by default.
Firefox Sync (which Zen uses under the hood) only covers part of the browser state:
- Bookmarks
- Passwords
- History
- Installed extensions
- Open tabs
It doesn't cover the customizations that matter most on Zen:
- Workspaces and containers
- Arc-style CSS (
userChrome.css,userContent.css) - Zen mods (
zen-themes.json+chrome/zen-themes/) about:configsettings (user.js,prefs.js)- Custom keyboard shortcuts
- Toolbar layout (
xulstore.json) - Essential tabs and pinned tabs
ZenSync covers all of that. You can use it alongside Firefox Sync — or instead of it, if you'd rather sync passwords/bookmarks through Git too (see What gets synced).
- Commands:
init,push,pull,config,purge-history - Declarative config (
config.toml, synced via the repo) + optional per-machine override (config.local.toml) — NixOS-style: same environment everywhere - Per-group control: what syncs (
sync) and what happens to local data of disabled groups on pull (on_pull = "preserve" | "clean") - Orphan detection: files from previous syncs that no longer match your config are flagged and removed (with confirmation)
- Optional history purge (
purge-history) to erase sensitive state from all past commits - Automatic detection of the active profile (Flatpak and native install)
- Automatic backup before every
pull - Defensive locks:
pullaborts if Zen is running;pushwarns if Zen is running withsessionsenabled - Absolute path sanitization (
/home/user/→{{HOME}})
zensync config --init creates config.toml inside the repo (so every machine shares the same rules). For per-machine overrides, create ~/.local/share/zensync/config.local.toml — it is never synced and wins over the shared file.
[groups.ui] # themes, mods, prefs, shortcuts, chrome/
sync = true
on_pull = "preserve"
[groups.extensions] # installed extensions + their settings
sync = true
[groups.containers] # container definitions (name/color/icon)
sync = true
[groups.passwords] # logins.json + key4.db
sync = true
[groups.bookmarks] # bookmarkbackups/ snapshots — NO browsing history
sync = true
[groups.sessions] # workspaces, open tabs, tab↔container, pins, live folders
sync = false
[groups.history] # places.sqlite, formhistory
sync = false
[groups.cookies] # active site logins
sync = false
[groups.storage] # site data (IndexedDB etc.)
sync = false
[extra]
include = [] # extra paths relative to the profile
exclude = [] # e.g. ["xulstore.json"] to skip window layoutsync = false→ the group never leaves/enters this machineon_pull = "preserve"→ local data of a disabled group is left untouchedon_pull = "clean"→ local data is deleted on pull (full NixOS-like purity; the automatic backup still covers you)
Want the exact same tabs/workspaces on every machine? Set sessions.sync = true. Sites will open logged-out (cookies stay off) but the structure comes over.
Bookmarks caveat: they sync via Zen's own bookmarkbackups/ snapshots. After a pull, restore once per machine: Ctrl+Shift+O → Import and Backup → Restore → pick the date. Snapshots are generated by Zen daily, so brand-new bookmarks may take up to a day to appear.
Group membership lives in src/files.rs. zensync config shows your effective setup.
The denylist in src/files.rs blocks transient state that either regenerates automatically or corrupts the target if copied:
- Lock files:
.parentlock,parent.lock,lock, any*.lock/*.lck - SQLite write-ahead logs: any
*-wal,*-shm,*-journal
These are also deliberately excluded (not on the allowlist):
cache2/,startupCache/,thumbnails/,shader-cache/— caches (regenerate)weave/— Firefox Sync internal state (syncing this causes conflicts)datareporting/,crashes/,minidumps/,logs/— telemetry / diagnosticsgmp-gmpopenh264/,gmp-widevinecdm/— DRM codecs (installed on demand, ~20 MB)
The passwords group is on by default — meaning your passwords go into the Git repo. Even if the repo is private, this means that if your GitHub account or SSH key is compromised, your passwords leak.
If you'd rather leave passwords to Firefox Sync (safer), set groups.passwords.sync = false.
If you synced before v0.1.0, your repo history contains sessions/cookies. push removes them from the current state (with confirmation), but old commits still have them. To erase them completely:
# requires git-filter-repo: https://github.com/newren/git-filter-repo
zensync purge-historyThis rewrites history (force-push). Other machines must re-init afterwards.
Files like places.sqlite, sessionstore.jsonlz4 etc. change constantly while Zen runs. push warns when Zen is open and the sessions group is enabled — prefer closing Zen first.
zensync pull uses git pull --ff-only. If you edited configs on two machines without syncing between them, the pull will fail. For now, resolve manually:
cd ~/.local/share/zensync/repo
git pull --rebase # or your preferred strategy- Linux (Fedora, Ubuntu, Arch — any distro; tested on Fedora 43)
- Rust (any recent stable; tested with 1.95)
- Git available in
PATH - Zen Browser installed (native or Flatpak — both autodetected)
- A GitHub account (or any Git host) with an empty private repository to store your configs
- An SSH key registered on GitHub (or HTTPS with a PAT)
- (optional) git-filter-repo — only needed for
purge-history
cargo install zensync# Install Rust if you don't have it
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
source ~/.cargo/env
# Clone the project
git clone https://github.com/DevPorfirio/ZenSync.git
cd ZenSync
# Build and install the binary to ~/.cargo/bin/zensync
cargo install --path .
# Confirm
zensync --helpCreate an empty private repository on GitHub (or GitLab, Codeberg, etc.):
- Go to
github.com/new - Name:
zen-configs(or whatever you prefer) - Visibility: Private
- Do not check "Add README", "Add .gitignore", or a license — it must be 100% empty
Note the SSH URL, something like: git@github.com:YOUR_USER/zen-configs.git.
ssh -T git@github.com
# Expected: "Hi USER! You've successfully authenticated..."If you get Permission denied, you need to add an SSH key to your GitHub account. Generate one if you don't have any:
ssh-keygen -t ed25519 -C "you@example.com"
cat ~/.ssh/id_ed25519.pubPaste the output into github.com/settings/ssh/new.
zensync init git@github.com:YOUR_USER/zen-configs.gitThis clones the (empty) repo into ~/.local/share/zensync/repo.
# Close Zen first (important because of the SQLite WAL)
pgrep -af zen
zensync pushDone — your configs are on GitHub.
# Send local changes to Git
zensync push # --yes to skip confirmations
# Pull changes from Git and apply (Zen must be closed)
zensync pull
# Show the effective config and where each layer comes from
zensync config # --init creates the shared config.toml
# Erase disabled groups (sessions, cookies...) from ALL git history
zensync purge-history # requires git-filter-repo; force-pushes
# Show help
zensync --helpRepeat Installation and Initial setup, but in step 4 use pull instead of push:
zensync init git@github.com:YOUR_USER/zen-configs.git
# close Zen
zensync pullpull creates a full backup of the current profile at ~/.local/share/zensync/backups/<timestamp>/ before overwriting anything, so you can revert manually if something goes wrong.
- Changed something on machine A →
zensync push - Before opening Zen on machine B →
zensync pull(with Zen closed) → open Zen
ZenSync/
├── Cargo.toml
├── plano.md # planning notes (Portuguese)
├── README.md # you are here
└── src/
├── main.rs # entry point, subcommand routing
├── cli.rs # clap definitions (subcommands and flags)
├── config.rs # layered config (shared + local override), groups
├── config_cmd.rs # `config` command
├── files.rs # group-tagged allowlist and denylist
├── state.rs # paths for the state dir (~/.local/share/zensync)
├── profile.rs # detects the active profile via profiles.ini
├── lifecycle.rs # checks whether Zen is running (sysinfo)
├── sanitize.rs # $HOME ↔ {{HOME}} transformation
├── git.rs # shell-out to git
├── sync.rs # copy engine (text/binary/recursive)
├── init.rs # init command
├── push.rs # push command
├── pull.rs # pull command
└── purge.rs # purge-history command
| Path | Purpose |
|---|---|
~/.local/share/zensync/repo/ |
Local clone of the configs repo (holds the shared config.toml) |
~/.local/share/zensync/config.local.toml |
Per-machine config override (never synced) |
~/.local/share/zensync/backups/<timestamp>/ |
Automatic backups before every pull |
~/.zen/ or ~/.var/app/app.zen_browser.zen/.zen/ |
Zen profile (we don't touch it directly — we resolve it via profiles.ini) |
MIT.