Turn a Chrome profile's browsing history into a cleaned navigation forest.
Project description
chrome-navtree
Turn a Chrome profile's browsing history into a cleaned navigation forest: the tree of pages you actually visited, with single-page-app churn collapsed, auth redirects spliced out, and junk destinations pruned.
Chrome's on-disk history already records how you got from page to page (referrer and tab-opener edges, plus a transition type per visit). This reconstructs that into a forest of trees, then cleans it so what is left is the shape of your actual journeys instead of every redirect and tab refresh.
This package is the engine. What you build on top of it (a viewer, an archiver, a note-taking integration) is yours to write. A self-contained HTML viewer ships in the box so you can see the result immediately.
Install
Run it with no install at all (needs uv):
uvx --from git+https://github.com/technicalpickles/chrome-navtree chrome-navtree profiles
Or install it as a CLI and library:
pip install chrome-navtree # or: uv add chrome-navtree
uv tool install chrome-navtree # isolated, on your PATH
Python 3.11+ (uses stdlib tomllib). No runtime dependencies.
Quick start
# List the Chrome profiles on this machine
chrome-navtree profiles
# Open an HTML viewer of the last 7 days for your "work" profile
chrome-navtree preview --profile work --since 7d --open
# Emit the cleaned tree as JSON for something else to consume
chrome-navtree dump --profile work --since 24h > forest.json
--since takes 7d, 24h, or an ISO date like 2026-07-01. --profile takes a profile key (from chrome-navtree profiles) or a raw directory name like Default.
The live Chrome database is never touched. It gets copied to a temp file and opened read-only, so it is safe to run while Chrome is open.
Library
Every consumer goes through the same three steps, so the viewer and the JSON output can never drift:
from datetime import timedelta
from chrome_navtree import discover_profiles, load_visits, build_forest, clean, Rules
prof = discover_profiles()["work"]
visits = load_visits(prof.history_path, since=timedelta(days=7))
raw = build_forest(visits) # faithful, unfiltered
forest = clean(raw, Rules.load("rules.toml")) # collapsed / spliced / pruned
forest.to_dict() # JSON-serializable: {"roots": [...], "stats": {...}}
clean is a pure function: it returns a new forest and never mutates its input.
What cleaning does
Three passes, in order:
- collapse folds churn to one node. A single-page app that keeps rewriting its URL, or a GitHub PR you clicked through 40 tabs of, becomes one node with a
visit_count. Folding is driven by rules (drop query and fragment, plus per-host path folds). - splice removes auth interstitials and reconnects their children to the parent, so a
real page -> SSO -> real pagechain stays one unbroken path instead of three. - prune drops junk destinations along with their whole subtree.
Rules
The engine carries no knowledge of your specific tools or workplace. What counts as junk, what counts as auth plumbing, and how paths fold all live in TOML.
The package ships default_rules.toml with generic defaults: query/fragment dropping, GitHub PR/issue folding, and common SSO providers (Google, Microsoft, Okta, WorkOS). Your own rules.toml layers on top, where list values append to the defaults so you only list your own additions:
[prune]
junk_hosts = ["calendar.example.com", "internal-dashboard.example.com"]
[splice]
auth_hosts = ["sso.mycorp.example"]
[[collapse.fold]]
host = "tickets.example.com"
pattern = "^(/browse/[A-Z]+-[0-9]+)"
See examples/rules.example.toml for a fuller template. Pass it with --rules rules.toml or Rules.load("rules.toml").
Data model
Visitis one row from Chrome'svisitstable, joined to its URL, with the timestamp and transition decoded.NavNodeis a node in the tree:url,title,host,transition,dwell_s,visit_count,key,children.NavForestisrootsplus summarystats(raw vs cleaned node counts, distinct hosts, spliced and pruned tallies).
Notes and limits
- Chrome expires history after about 90 days, so that is the practical window. Archiving beyond it is a job for a consumer built on this engine, not the engine itself.
- Auth detection is deliberately aggressive: anything titled "sign in" or "log in", plus common auth URL shapes and identity-provider hosts, gets spliced. For a work browsing corpus that is the right trade. If a real page ever vanishes, narrow the
splicerules.
Development
uv sync --extra dev # ruff, mypy, pytest
uv run --extra dev pytest
mise run check # ruff + ruff format + mypy + pytest (what CI runs)
Tooling is pinned with mise and git hooks run through hk. After cloning, mise install then hk install gives you ruff + mypy on every commit. If you would rather not use mise, the checks are plain uv run --extra dev ... commands.
Test data, isolated from your real Chrome
The live Chrome database is never touched (it is copied to a temp file and opened read-only), and none of the dev tooling reads or writes a real Chrome profile. Tests build synthetic Chrome databases in a temp dir and assert an exact cleaned forest, so they are deterministic and need no real browser data.
To poke at the CLI by hand, scaffold a fake Chrome profile (--chrome-root goes after the subcommand):
python scripts/fake_chrome.py /tmp/fake-chrome
chrome-navtree profiles --chrome-root /tmp/fake-chrome
chrome-navtree dump --chrome-root /tmp/fake-chrome --profile work
For realistic data, scripts/seed_realistic.py drives a throwaway Chromium profile (its own --user-data-dir, never your real one) with Playwright and snapshots the history into tests/fixtures/realistic/:
uv sync --extra playwright
uv run playwright install chromium
uv run python scripts/seed_realistic.py --scripted # or --manual to click around yourself
Playwright is an opt-in extra and never runs in CI.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters