Serverless cross-browser bookmark sync. Bookmarks live as a JSON file in your own private GitHub repo; browser extensions and a web UI both talk directly to the GitHub Contents API. No server, no backend, no infrastructure to host. You own your data — it's just a file in a repo you control.
Status: Chrome extension is functional end-to-end (save via toolbar
button, save all open tabs in one action, two-way sync with the native
bookmark tree, 5-min poll for remote changes, automatic conflict retry).
Firefox MV3 add-on shipping the same
source as Chrome via a shared package. Web UI (list, search, tag management,
bulk operations, trash, Netscape HTML export, sign out) deploys as a static
SPA. Safari is next in the roadmap. See spec.md for the full design.
- Save the current tab to GitHub via the toolbar button
- Save all open tabs in the current window in one action — one batched
bookmarks.jsonwrite, grouped under a datedSession YYYY-MM-DDfolder, with exact-URL de-dupe and browser-internal tabs skipped - Imports your existing browser bookmarks automatically when you finish setup (also on browser startup and repo switch) — see Importing existing bookmarks
- Two-way native bookmark-tree sync:
- Drag/add a bookmark in the browser → appears in
bookmarks.jsonwithin ~1s - Edit a bookmark's title → remote updates within ~1s
- Delete a bookmark → soft-deleted (tombstone); GC'd from the JSON after 30 days but retained in git history forever
- Drag/add a bookmark in the browser → appears in
- Remote → local pull: edits made directly on GitHub (or from another device) pull into the browser on a 5-minute poll
- Automatic conflict resolution via GitHub's file SHA + optimistic retry-replay
- Optional tracking-param stripping (utm_*, fbclid, gclid, …) at save time — opt-in
- One-click link to the web UI from the popup
- Dark cyan/magenta themed popup + options pages, matching the web UI
Web UI (static SPA — https://paperhurts.github.io/gitmarks/)
- List + full-text search across title, URL, tags, and notes
- Tag management: rename, recolor, add, delete
- Bulk operations: multi-select → add/remove tag, set folder, soft-delete
- Trash with restore (30-day soft-delete window)
- Netscape HTML export — a standard bookmarks file you can re-import anywhere
- Sign out — clears your PAT/settings from this machine's local storage
- No server, ever — clients talk to the GitHub REST API directly; your data
is a
bookmarks.json/tags.jsonpair in your own private repo, with git history as the audit log - 311 automated unit + component tests + 6 Playwright e2e (against real Chromium)
| Package | Role |
|---|---|
@gitmarks/core |
Shared TypeScript library: schemas (Zod), GitHub Contents API client with optimistic concurrency, ULID + URL helpers, pure mutation helpers |
@gitmarks/extension-shared |
Cross-browser extension source — popup, options, background, lib/ helpers. Consumed by both browser shells via workspace:*. 119 unit tests live here. |
@gitmarks/extension-chrome |
Chrome MV3 shell. Manifest + Vite/crxjs build + Playwright e2e. Thin entry files import from extension-shared. |
@gitmarks/extension-firefox |
Firefox MV3 shell. Manifest + plain Vite build. Same source as Chrome via extension-shared. Load via about:debugging. |
@gitmarks/web |
Static SPA — list, search, tag management, bulk operations, trash, Netscape HTML export, sign out. Vite + React + Tailwind. Talks directly to GitHub via @gitmarks/core. Deploys to GitHub Pages or Cloudflare Pages. |
The read-side web UI is auto-deployed to GitHub Pages: https://paperhurts.github.io/gitmarks/
You'll need a fine-grained PAT (see "Your data, your PAT" below) and your own private bookmarks repo. The web UI runs entirely in your browser — no server sees your token.
From the browser you're installing into — automatic. When you finish setup,
the extension reconciles your existing native bookmark tree against the repo and
pushes everything not already there into bookmarks.json in one batched write
(unsafe-scheme URLs skipped, deduped by URL). This also runs on browser startup
and when you change the configured repo. (No banner/error in the popup means it
succeeded or hasn't run yet; if bookmarks are missing, open the popup to check
for a background-sync error.)
From a bookmarks export file (Netscape HTML) — there's no direct file
importer yet. The simplest path is to import the HTML into your browser first
(Firefox: Ctrl+Shift+O → Import and Backup → Import Bookmarks from HTML;
Chrome: Bookmarks → Import bookmarks and settings), then the extension syncs
those into the repo exactly as above. The web UI's Export produces this same
Netscape format, so it round-trips.
gitmarks authenticates to GitHub with a fine-grained personal access token scoped to only your bookmarks repo. To create one:
- First, create a private repo on github.com to hold your bookmarks
(e.g.
my-bookmarks). It can be empty — gitmarks createsbookmarks.jsonon first save. - Go to https://github.com/settings/personal-access-tokens/new (github.com → your avatar → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token).
- Token name: anything (e.g.
gitmarks). Set an Expiration you're comfortable with. - Repository access: select Only select repositories → pick your bookmarks repo only.
- Permissions → Repository permissions → Contents: set to Read and write. (That's the only permission needed; leave everything else default.)
- Click Generate token and copy it (
github_pat_…) — you won't see it again. Paste it into the extension's setup screen.
Why fine-grained + single-repo: if your browser profile is ever compromised, the token only unlocks your bookmarks repo, not your whole GitHub account. See Your data, your PAT for storage and revocation details.
pnpm install
pnpm --filter @gitmarks/extension-chrome buildThen in Chrome:
chrome://extensions/→ toggle Developer mode on- Load unpacked → select
packages/extension-chrome/dist/ - Click the toolbar icon → "Set up gitmarks"
- Paste your fine-grained PAT (see Creating your GitHub token), enter owner/repo/branch, click Save
See packages/extension-chrome/README.md for the full setup walkthrough,
the manual smoke test checklist, and architecture notes.
- The repo must be private. Public repo + the project name = anyone can find your bookmarks. The extension does NOT enforce this — it's on you when you create the repo on github.com.
- Use a fine-grained PAT scoped to only your bookmarks repo with only Contents: read/write. Never use a classic PAT or one with broader scopes — if your browser profile is ever exfiltrated, that token only unlocks your bookmarks, not your whole GitHub account.
- The PAT is stored in
chrome.storage.local, which is origin-scoped (other extensions / sites can't read it) but readable by anyone with access to your unlocked browser profile. Treat it like a saved password. - No telemetry. The extension only talks to
api.github.com. That's enforced by the MV3 manifest'shost_permissions.
When you stop using gitmarks (uninstall the extension, clear browser data, or switch machines):
- Revoke the PAT on github.com. Settings → Developer settings → Personal access tokens → Fine-grained tokens → find the one named for your bookmarks repo → Delete. This is the only authoritative way to invalidate the credential.
- Web UI: click Sign out in the header. This clears
localStorageon your current machine. (It does NOT revoke the token on GitHub — see step 1.) - Extension: uninstalling the extension removes its
chrome.storage.localentry on that machine. The token on GitHub remains valid until you revoke it.
Treat the PAT like a saved password. If a machine is lost or compromised, revoke immediately on github.com.
# Everything
pnpm install
pnpm test # all unit tests across packages
pnpm typecheck
pnpm build
# Just one package
pnpm --filter @gitmarks/core test
pnpm --filter @gitmarks/extension-shared test # all extension unit tests live here
pnpm --filter @gitmarks/extension-chrome e2e # Playwright + real ChromiumThe repo is a pnpm workspace monorepo. Each package has its own
README.md with package-specific docs.
[Chrome ext] [Firefox ext] [Safari ext (planned)] [Web UI]
\ | / /
\ | / /
v v v v
GitHub REST API (api.github.com)
|
v
User's private repo: bookmarks.json + tags.json
The load-bearing invariants:
- No server, ever. Clients talk to GitHub REST API directly. PAT
lives client-side (
chrome.storage.local). - Optimistic concurrency via GitHub file SHA. On 409, the core client refetches and replays the mutation (up to 3 attempts with exponential backoff).
- Eventual consistency, ~30s target. Event-driven push for local
changes (500ms debounce). 5-minute poll for remote changes via
chrome.alarms, with ETag conditional reads so unchanged polls cost nothing against the rate limit. - Soft deletes (tombstones) for ~30 days; git history retains everything forever.
- Suppression registry prevents loop-back: when the extension applies
a remote change to
chrome.bookmarks, the affected URL is parked in an in-memory registry for ~2 seconds so the resulting local event doesn't echo back to GitHub.
- ✅
@gitmarks/core— schemas, GitHub client, mutations - ✅ Chrome MVP — toolbar-button save flow
- ✅ Chrome native tree integration — listeners, reconcile, poll loop
- ✅ Tracking-param stripping (opt-in)
- ✅ Firefox MV3 add-on (#23)
- ✅ Web UI v1: list + search + tag management (#24)
- ✅ Web UI v2: bulk operations + trash + export (#25)
- ✅ Bookmark all open tabs in one action (#46)
- ✅ Popup polish: web-UI theme, auto-dismiss after save, "Open web UI" link
- ⬜ Safari (#26)
spec.md— full design spec (source of truth for non-obvious decisions)CONTRIBUTING.md— branch/PR conventions, TDD policy, plan-driven workflowCLAUDE.md— guidance for AI agents working in this repoLICENSE— MITdocs/superpowers/plans/— implementation plans, one per branchpackages/*/README.md— package-specific documentationexamples/example-bookmarks-repo/— samplebookmarks.json+tags.jsonto seed a fresh repo, used by@gitmarks/corefixture tests.github/workflows/test.yml— CI (typecheck + unit tests + build on every PR)
See CONTRIBUTING.md for the branch/PR conventions, conventional-commit
scopes, and the plan-driven workflow used for larger features. Every
change goes through a PR with green CI — no direct commits to main.