Skip to content

Instantly share code, notes, and snippets.

@medhatdawoud
Created May 30, 2026 23:28
Show Gist options
  • Select an option

  • Save medhatdawoud/3cf1adabe4d5bc46cc869828526b94ba to your computer and use it in GitHub Desktop.

Select an option

Save medhatdawoud/3cf1adabe4d5bc46cc869828526b94ba to your computer and use it in GitHub Desktop.
Export Zen browser workspaces to Chrome-importable HTML bookmarks
#!/usr/bin/env python3
"""
Export Zen browser workspaces to Chrome-importable HTML bookmarks.
Each workspace becomes a bookmark folder; all tabs become bookmarks inside.
Usage:
python3 zen-to-html-bookmarks.py [output.html]
Requires: lz4 (pip install lz4)
"""
import json
import lz4.block
import os
import struct
import sys
from collections import defaultdict
from pathlib import Path
ZEN_PROFILE = None # auto-detected from profiles.ini
def find_zen_profile():
base = Path.home() / "Library/Application Support/zen/Profiles"
if not base.exists():
return None
# Prefer "Default (release)" if it exists
for name in base.iterdir():
if "release" in name.name.lower():
return name / "zen-sessions.jsonlz4"
# Fall back to first profile found
profiles = list(base.iterdir())
if profiles:
return profiles[0] / "zen-sessions.jsonlz4"
return None
def read_mozlz4(path):
with open(path, "rb") as f:
magic = f.read(8)
if magic != b"mozLz40\x00":
raise ValueError(f"Not a mozlz4 file: {path}")
size = struct.unpack("<I", f.read(4))[0]
return lz4.block.decompress(f.read(), uncompressed_size=size)
def tab_url_title(tab):
entries = tab.get("entries", [])
if not entries:
return None, None
idx = min(tab.get("index", len(entries) - 1), len(entries) - 1)
entry = entries[idx]
url = entry.get("url", "")
title = entry.get("title", "") or url
return url, title
def extract_spaces(d):
space_map = {s["uuid"]: s["name"] for s in d["spaces"]}
tabs_by_space = defaultdict(list)
for tab in d["tabs"]:
if tab.get("zenIsEmpty"):
continue
url, title = tab_url_title(tab)
if not url or url.startswith("about:") or url.startswith("chrome:"):
continue
ws = tab.get("zenWorkspace")
tabs_by_space[ws].append({"url": url, "title": title})
spaces = []
for space in d["spaces"]:
uuid = space["uuid"]
name = space["name"] or "Unnamed"
tabs = tabs_by_space.get(uuid, [])
spaces.append({"name": name, "tabs": tabs})
return spaces
def make_html(spaces):
def esc(s):
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
lines = [
"<!DOCTYPE NETSCAPE-Bookmark-file-1>",
'<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">',
"<TITLE>Bookmarks</TITLE>",
"<H1>Bookmarks</H1>",
"<DL><p>",
" <DT><H3>Zen Workspaces</H3>",
" <DL><p>",
]
for space in spaces:
lines.append(f" <DT><H3>{esc(space['name'])}</H3>")
lines.append(" <DL><p>")
for tab in space["tabs"]:
lines.append(f' <DT><A HREF="{esc(tab["url"])}">{esc(tab["title"])}</A>')
lines.append(" </DL><p>")
lines += [" </DL><p>", "</DL>"]
return "\n".join(lines)
def main():
data_path = find_zen_profile()
if not data_path or not data_path.exists():
print("Zen session file not found. Is Zen installed?")
sys.exit(1)
args = sys.argv[1:]
out = Path(args[0]) if args else Path("/tmp/zen_workspaces.html")
print(f"Reading {data_path} ...")
raw = read_mozlz4(data_path)
d = json.loads(raw)
spaces = extract_spaces(d)
print(f"Found {len(spaces)} workspaces:")
for s in spaces:
print(f" {s['name']} ({len(s['tabs'])} tabs)")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(make_html(spaces))
print(f"\nHTML written to: {out}")
print("Import via Chrome: chrome://bookmarks → ⋮ → Import bookmarks")
os.system(f'open "{out}"')
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment