Skip to content

Instantly share code, notes, and snippets.

@medhatdawoud
Last active May 30, 2026 22:14
Show Gist options
  • Select an option

  • Save medhatdawoud/04e429a4b0d618a480a91985a571cb99 to your computer and use it in GitHub Desktop.

Select an option

Save medhatdawoud/04e429a4b0d618a480a91985a571cb99 to your computer and use it in GitHub Desktop.
Convert Arc browser spaces to Chrome bookmark folders (with pinned tabs). Run with --html flag to generate importable HTML.
#!/usr/bin/env python3
"""
Convert Arc browser spaces to Chrome bookmark folders.
Each space becomes a bookmark folder; pinned tabs become bookmarks inside.
Run before launching Chrome for the first time on a new machine.
"""
import json
import os
import re
import sys
import time
from pathlib import Path
ARC_DATA = Path.home() / "Library/Application Support/Arc/StorableSidebar.json"
CHROME_DIR = Path.home() / "Library/Application Support/Google/Chrome/Default"
CHROME_BKM = CHROME_DIR / "Bookmarks"
def clean_url(url):
return url.replace("\\/", "/")
SKIP_SPACE_IDS = {"thebrowser.company.defaultPersonalSpaceID"}
def extract_spaces(d):
sync = d["firebaseSyncState"]["syncData"]
# Build item lookup: id -> value dict
items = sync["items"]
lookup = {}
i = 0
while i < len(items) - 1:
if isinstance(items[i], str) and isinstance(items[i+1], dict):
val = items[i+1].get("value", items[i+1])
lookup[items[i]] = items[i+1]
i += 2 if (isinstance(items[i], str) and i+1 < len(items) and isinstance(items[i+1], dict)) else 1
# Parse space models
space_models = sync["spaceModels"]
spaces = []
i = 0
while i < len(space_models) - 1:
if isinstance(space_models[i], str) and isinstance(space_models[i+1], dict):
sid = space_models[i]
data = space_models[i+1]
if sid not in SKIP_SPACE_IDS:
raw = json.dumps(data)
tm = re.search(r'"title"\s*:\s*"([^"]+)"', raw)
title = tm.group(1) if tm else sid[:8]
# Find pinned container ID
val = data.get("value", data)
pinned_cid = None
for cid in val.get("containerIDs", []):
if isinstance(cid, str) and len(cid) == 36:
# peek at next element to see if it's the pinned one
pass
new_cids = val.get("newContainerIDs", [])
for j, c in enumerate(new_cids):
if isinstance(c, dict) and "pinned" in c and j+1 < len(new_cids):
pinned_cid = new_cids[j+1] if isinstance(new_cids[j+1], str) else None
break
# Collect pinned tab URLs
bookmarks = []
if pinned_cid and pinned_cid in lookup:
container = lookup[pinned_cid]
cv = container.get("value", container)
for child_id in cv.get("childrenIds", []):
if child_id in lookup:
tab_raw = json.dumps(lookup[child_id])
url_m = re.search(r'"savedURL"\s*:\s*"([^"]+)"', tab_raw)
ttl_m = re.search(r'"savedTitle"\s*:\s*"([^"]+)"', tab_raw)
if url_m:
bookmarks.append({
"url": clean_url(url_m.group(1)),
"title": ttl_m.group(1) if ttl_m else clean_url(url_m.group(1))
})
spaces.append({"title": title, "bookmarks": bookmarks})
i += 2
else:
i += 1
return spaces
def make_chrome_bookmarks(spaces):
ts = str(int(time.time() * 1000000 + 11644473600000000))
uid = [10]
def next_id():
uid[0] += 1
return str(uid[0])
def make_folder(name, children):
return {"children": children, "date_added": ts, "date_modified": ts,
"id": next_id(), "name": name, "type": "folder"}
def make_url(title, url):
return {"date_added": ts, "id": next_id(), "name": title, "type": "url", "url": url}
folders = []
for space in spaces:
children = [make_url(b["title"], b["url"]) for b in space["bookmarks"]]
folders.append(make_folder(space["title"], children))
arc_folder = make_folder("Arc Spaces", folders)
return {
"checksum": "",
"roots": {
"bookmark_bar": make_folder("Bookmarks bar", [arc_folder]),
"other": make_folder("Other bookmarks", []),
"synced": make_folder("Mobile bookmarks", [])
},
"version": 1
}
def make_html(spaces):
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>Arc Spaces</H3>",
" <DL><p>",
]
for space in spaces:
lines.append(f" <DT><H3>{space['title']}</H3>")
lines.append(" <DL><p>")
for b in space["bookmarks"]:
title = b["title"].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
url = b["url"].replace("&", "&amp;")
lines.append(f' <DT><A HREF="{url}">{title}</A>')
lines.append(" </DL><p>")
lines += [" </DL><p>", "</DL>"]
return "\n".join(lines)
def main():
if not ARC_DATA.exists():
print(f"Arc data not found at {ARC_DATA}")
print("Run this script on a machine with Arc installed.")
sys.exit(1)
html_mode = "--html" in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith("--")]
print("Reading Arc spaces...")
with open(ARC_DATA) as f:
d = json.load(f)
spaces = extract_spaces(d)
print(f"Found {len(spaces)} active spaces:")
for s in spaces:
print(f" {s['title']} ({len(s['bookmarks'])} pinned tabs)")
if html_mode:
out = Path(args[0]) if args else Path("/tmp/arc_spaces.html")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(make_html(spaces))
print(f"\nHTML bookmarks written to: {out}")
print("Import via Chrome: chrome://bookmarks → ⋮ menu → Import bookmarks")
os.system(f'open "{out}"')
else:
out = Path(args[0]) if args else Path("/tmp/chrome_bookmarks_from_arc.json")
out.parent.mkdir(parents=True, exist_ok=True)
with open(out, "w") as f:
json.dump(make_chrome_bookmarks(spaces), f, indent=2)
print(f"\nJSON bookmarks written to: {out}")
if __name__ == "__main__":
main()
@medhatdawoud

Copy link
Copy Markdown
Author

From the gist directly (no clone needed):
curl -sL https://gist.github.com/medhatdawoud/04e429a4b0d618a480a91985a571cb99/raw | python3 - --html ~/arc_spaces.html

Then in Chrome:

  1. chrome://bookmarks
  2. ⋮ → Import bookmarks
  3. Select ~/arc_spaces.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment