| name | arc-to-zen-migration |
| description | Use when migrating Arc browser spaces/workspaces into Zen Browser, or bulk-creating Zen workspaces, folders, essentials, or pinned tabs by editing the session store. Covers reading/writing Zen's mozlz4 session file and its folder/group/space schema. |
Arc โ Zen Migration
Overview
Zen has no Arc importer. But Zen (Firefox-based) keeps its whole tab/folder/workspace state in one compressed file you can script against, so Arc spaces can be rebuilt in bulk โ folders become folders, not flat pins.
Read side (Arc): ~/Library/Application Support/Arc/StorableSidebar.json โ plain JSON holding every space, folder, and pinned tab. No manual pasting needed.
Write side (Zen): <profile>/zen-sessions.jsonlz4 โ Mozilla-lz4 (mozLz40\0) compressed JSON.
Tools:
scripts/arc_extract.py โ reads Arc's sidebar, returns each space as a nested tree.
scripts/zen_session.py โ read/write Zen's mozlz4 session + folder/tab/essential builders.
scripts/migrate.py โ end-to-end: Arc space โ Zen workspace (preserves nested folders).
scripts/migrate_example.py โ manual fill-in template (when there's no Arc file, e.g. user pastes URLs).
When to Use
- "Migrate my Arc workspaces/spaces to Zen"
- "Bulk add folders / pinned tabs / essentials to Zen"
- Recreating Arc's folder structure inside a Zen workspace
Not for: bookmarks (use Arc export HTML โ Zen bookmark import) or passwords (use a password manager / keychain export).
The Iron Rule
Zen MUST be fully quit (Cmd+Q) before writing. It rewrites the session on exit and will clobber your changes otherwise. Always backup() first โ a wrong schema corrupts the session; the .bak restores instantly.
Workflow (automatic โ Arc installed)
- Quit Zen (Cmd+Q) โ non-negotiable (see Iron Rule).
- Preview:
python3 scripts/migrate.py --list prints Arc spaces โ Zen workspaces.
- Migrate everything from zero:
python3 scripts/migrate.py --all --favorites
โ creates a Zen workspace per Arc space (with Arc's emoji icon), imports folders + pins, and adds Arc's global Favorites as global essentials.
- Or one space:
python3 scripts/migrate.py "Home" --favorites (creates the workspace if missing; --zen "Name" maps to a differently-named one; --essentials N promotes the first N top-level pins to essentials).
- Reopen Zen, verify. Re-runnable โ
clear_workspace wipes the target first. Restore the .bak if wrong.
Mapping Arc โ Zen: space list โ folder (nesting preserved) ยท space top-level tab โ regular pin ยท global Favorites (topApps) โ global essentials.
Workflow (manual โ no Arc file, user pastes URLs)
Use migrate_example.py: fill ESSENTIALS/PINS/FOLDERS, quit Zen, run. Screenshots of the Arc sidebar are enough to fill it.
If Zen's schema drifted (script misbehaves): make ONE folder with 2 tabs by hand in Zen, quit, zen_session.py <profile> --dump, compare the live folders[]/groups[]/tabs[] shapes, update the builders.
Schema (Zen 1.20.x / Firefox 151)
A folder = two synced records sharing one id, plus member tabs:
| Where | Key fields |
|---|
folders[] | id, name, workspaceId, collapsed, parentId:null, emptyTabIds:[], userIcon |
groups[] | id (same), name, color:"zen-workspace-color", collapsed |
tabs[] (member) | groupId:<folder id>, zenWorkspace:<space uuid>, pinned:true, zenEssential:false |
- Essential:
zenEssential:true. Scoped by CONTAINER (userContextId), not zenWorkspace โ this is the #1 gotcha. With zen.workspaces.separate-essentials=true (Zen default), an essential shows only in workspaces whose containerTabId matches its userContextId; userContextId:0 = the default container = the first/Home workspace. To make essentials global (Arc-style, same in every space), set zen.workspaces.separate-essentials=false (via user.js) and use userContextId:0 / zenWorkspace:null. For a per-workspace essential, set userContextId to that workspace's containerTabId.
- Plain pin:
zenEssential:false, zenWorkspace:<uuid>, no groupId. Scoped by zenWorkspace (works with userContextId:0).
- Workspace lives in
spaces[]: {uuid, name, icon(emoji), containerTabId, position, theme}. ensure_workspace() creates one if absent.
- Ordering = tab array order.
tab.index is the active history-entry pointer (set to 1), NOT position.
- IDs are
<epoch_ms>-<counter>. Tabs need only a minimal entries:[{url,title,triggeringPrincipal_base64:"{\"3\":{}}"}]; Firefox fills the rest on load.
- Skip
arc://* URLs โ they don't resolve in Zen.
Library Quickref
from zen_session import ZenSession, find_profiles
s = ZenSession(find_profiles()[0])
s.backup()
home = s.workspace_by_name("Home")
s.clear_folders(workspace=home)
s.add_essential("https://discord.com/")
s.add_tab("https://news.com/", workspace=home)
fid = s.add_folder("Finance", home)
s.add_tab("https://sheet/", workspace=home, folder_id=fid)
s.save()
Common Mistakes
- Writing while Zen runs โ changes vanish on quit. Quit first.
- Folder in only one array โ ghost/empty folder. Always write BOTH
folders[] and groups[] (the library does).
- Tab missing
zenWorkspace โ tab lands in the wrong/no space. Set it (essentials use null).
- liblz4 not found โ
brew install lz4 (mac) / apt install liblz4-1 (linux); fix the path list in zen_session.py if needed.
- No backup โ re-run with
backup(); keep the .bak until verified.