tamnd/kagePublic

Shadow any website for offline viewing, with the JavaScript stripped out

AI summary: A headless browser tool to clone and archive dynamic websites for offline viewing with all JavaScript stripped out.

Stars
3.5K
+2 today
Forks
134
Watchers
13
Open issues
8
Open PRs
1
Contributors
~7
Commits
111
Branches
9

GoMITCreated Jun 14, 2026Last push 1mo agoLatest release v0.3.12+24 stars this week+93 this month

Quick answers

What is kage?
A headless browser tool to clone and archive dynamic websites for offline viewing with all JavaScript stripped out.
What does kage do?
Kage functions as a sophisticated web archiver that creates static, offline-readable clones of modern, highly dynamic websites. It achieves this by launching a real headless Chrome instance, waiting for JavaScript frameworks to fully render the page, and then taking a precise snapshot of the final DOM structure. Crucially, it then strips away all executable JavaScript while preserving and localizing the CSS, web fonts, and images. The resulting output is a completely static folder that perfectly mirrors the visual state of the live site but is entirely safe, lightweight, and capable of being hosted anywhere without dynamic backends.
Who is kage for?
This tool is ideal for developers, researchers, and digital archivists who need a reliable way to capture and secure high-fidelity, static snapshots of complex modern web applications.
How do I get started with kage?
npm install -g kage
How popular is kage on GitHub?
tamnd/kage has 3,456 stars and 134 forks on GitHub, and gained 24 stars in the last 7 days.
What license does kage use?
tamnd/kage is released under the MIT license.

Star history

since Jul 29, 2026
01K2K3KJul 2026Aug 2026Sep 2026Oct 2026
3.5K stars as of Oct 3, 2026. Measured daily since Jul 29, 2026; GitHub no longer exposes earlier star timestamps.

Contribution activity

commits per day, last 52 weeks
SepOctNovDecJanFebMarAprMayJunJulAugSepMonWedFri2025-09-28: 0 commits2025-09-29: 0 commits2025-09-30: 0 commits2025-10-01: 0 commits2025-10-02: 0 commits2025-10-03: 0 commits2025-10-04: 0 commits2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-08: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 0 commits2026-02-03: 0 commits2026-02-04: 0 commits2026-02-05: 0 commits2026-02-06: 0 commits2026-02-07: 0 commits2026-02-08: 0 commits2026-02-09: 0 commits2026-02-10: 0 commits2026-02-11: 0 commits2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 0 commits2026-02-15: 0 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 0 commits2026-02-19: 0 commits2026-02-20: 0 commits2026-02-21: 0 commits2026-02-22: 0 commits2026-02-23: 0 commits2026-02-24: 0 commits2026-02-25: 0 commits2026-02-26: 0 commits2026-02-27: 0 commits2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 0 commits2026-03-04: 0 commits2026-03-05: 0 commits2026-03-06: 0 commits2026-03-07: 0 commits2026-03-08: 0 commits2026-03-09: 0 commits2026-03-10: 0 commits2026-03-11: 0 commits2026-03-12: 0 commits2026-03-13: 0 commits2026-03-14: 0 commits2026-03-15: 0 commits2026-03-16: 0 commits2026-03-17: 0 commits2026-03-18: 0 commits2026-03-19: 0 commits2026-03-20: 0 commits2026-03-21: 0 commits2026-03-22: 0 commits2026-03-23: 0 commits2026-03-24: 0 commits2026-03-25: 0 commits2026-03-26: 0 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 0 commits2026-03-30: 0 commits2026-03-31: 0 commits2026-04-01: 0 commits2026-04-02: 0 commits2026-04-03: 0 commits2026-04-04: 0 commits2026-04-05: 0 commits2026-04-06: 0 commits2026-04-07: 0 commits2026-04-08: 0 commits2026-04-09: 0 commits2026-04-10: 0 commits2026-04-11: 0 commits2026-04-12: 0 commits2026-04-13: 0 commits2026-04-14: 0 commits2026-04-15: 0 commits2026-04-16: 0 commits2026-04-17: 0 commits2026-04-18: 0 commits2026-04-19: 0 commits2026-04-20: 0 commits2026-04-21: 0 commits2026-04-22: 0 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 0 commits2026-04-28: 0 commits2026-04-29: 0 commits2026-04-30: 0 commits2026-05-01: 0 commits2026-05-02: 0 commits2026-05-03: 0 commits2026-05-04: 0 commits2026-05-05: 0 commits2026-05-06: 0 commits2026-05-07: 0 commits2026-05-08: 0 commits2026-05-09: 0 commits2026-05-10: 0 commits2026-05-11: 0 commits2026-05-12: 0 commits2026-05-13: 0 commits2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 0 commits2026-05-18: 0 commits2026-05-19: 0 commits2026-05-20: 0 commits2026-05-21: 0 commits2026-05-22: 0 commits2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 0 commits2026-05-26: 0 commits2026-05-27: 0 commits2026-05-28: 0 commits2026-05-29: 0 commits2026-05-30: 0 commits2026-05-31: 0 commits2026-06-01: 0 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 0 commits2026-06-05: 0 commits2026-06-06: 0 commits2026-06-07: 0 commits2026-06-08: 0 commits2026-06-09: 0 commits2026-06-10: 0 commits2026-06-11: 0 commits2026-06-12: 0 commits2026-06-13: 0 commits2026-06-14: 11 commits2026-06-15: 28 commits2026-06-16: 6 commits2026-06-17: 3 commits2026-06-18: 0 commits2026-06-19: 12 commits2026-06-20: 1 commit2026-06-21: 1 commit2026-06-22: 1 commit2026-06-23: 1 commit2026-06-24: 0 commits2026-06-25: 0 commits2026-06-26: 0 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 1 commit2026-06-30: 0 commits2026-07-01: 0 commits2026-07-02: 0 commits2026-07-03: 0 commits2026-07-04: 0 commits2026-07-05: 0 commits2026-07-06: 0 commits2026-07-07: 1 commit2026-07-08: 1 commit2026-07-09: 0 commits2026-07-10: 0 commits2026-07-11: 1 commit2026-07-12: 0 commits2026-07-13: 0 commits2026-07-14: 0 commits2026-07-15: 0 commits2026-07-16: 0 commits2026-07-17: 0 commits2026-07-18: 0 commits2026-07-19: 0 commits2026-07-20: 0 commits2026-07-21: 0 commits2026-07-22: 0 commits2026-07-23: 0 commits2026-07-24: 0 commits2026-07-25: 0 commits2026-07-26: 0 commits2026-07-27: 0 commits2026-07-28: 0 commits2026-07-29: 0 commits2026-07-30: 0 commits2026-07-31: 1 commit2026-08-01: 1 commit2026-08-02: 0 commits2026-08-03: 0 commits2026-08-04: 0 commits2026-08-05: 0 commits2026-08-06: 4 commits2026-08-07: 0 commits2026-08-08: 0 commits2026-08-09: 0 commits2026-08-10: 12 commits2026-08-11: 0 commits2026-08-12: 0 commits2026-08-13: 0 commits2026-08-14: 0 commits2026-08-15: 0 commits2026-08-16: 0 commits2026-08-17: 0 commits2026-08-18: 0 commits2026-08-19: 0 commits2026-08-20: 0 commits2026-08-21: 0 commits2026-08-22: 0 commits2026-08-23: 0 commits2026-08-24: 0 commits2026-08-25: 0 commits2026-08-26: 0 commits2026-08-27: 0 commits2026-08-28: 0 commits2026-08-29: 0 commits2026-08-30: 0 commits2026-08-31: 0 commits2026-09-01: 0 commits2026-09-02: 0 commits2026-09-03: 0 commits2026-09-04: 0 commits2026-09-05: 0 commits2026-09-06: 0 commits2026-09-07: 0 commits2026-09-08: 0 commits2026-09-09: 0 commits2026-09-10: 0 commits2026-09-11: 0 commits2026-09-12: 0 commits2026-09-13: 0 commits2026-09-14: 0 commits2026-09-15: 0 commits2026-09-16: 0 commits2026-09-17: 0 commits2026-09-18: 0 commits2026-09-19: 0 commits2026-09-20: 0 commits2026-09-21: 0 commits2026-09-22: 0 commits2026-09-23: 0 commits2026-09-24: 0 commits2026-09-25: 0 commits2026-09-26: 0 commits
86 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Breakout launch

    3,456 stars in 112 days

  • Permissive license

    MIT

  • Continuous integration

    Automated checks passing

What kage does

Kage functions as a sophisticated web archiver that creates static, offline-readable clones of modern, highly dynamic websites. It achieves this by launching a real headless Chrome instance, waiting for JavaScript frameworks to fully render the page, and then taking a precise snapshot of the final DOM structure. Crucially, it then strips away all executable JavaScript while preserving and localizing the CSS, web fonts, and images. The resulting output is a completely static folder that perfectly mirrors the visual state of the live site but is entirely safe, lightweight, and capable of being hosted anywhere without dynamic backends.

This tool is ideal for developers, researchers, and digital archivists who need a reliable way to capture and secure high-fidelity, static snapshots of complex modern web applications.

  • Headless Chrome Rendering: Executes full browser environments to accurately capture sites heavily reliant on React, Vue, or Angular for initial rendering.
  • JavaScript Eradication: Systematically removes all scripts from the cloned output to ensure security, improve performance, and guarantee static hosting compatibility.
  • Asset Localization: Automatically downloads and rewrites paths for all external CSS, images, and font files to enable true offline browsing.
  • Visual Fidelity: Snapshots the exact DOM state a human user would see after all network requests and layout shifts have settled.
  • Command Line Interface: Provides a streamlined CLI tool for easy integration into archiving scripts and automated backup workflows.

Where teams use it

Offline Documentation

Developers use the tool to download and archive complex, dynamic API documentation sites for reading during flights or network outages.

Static Archiving

Digital archivists preserve the exact visual state of dynamic news articles or interactive reports before they are modified or taken down.

Security Analysis

Security researchers snapshot potentially malicious sites into safe, static HTML representations for risk-free local inspection.

Lightweight Mirroring

Webmasters convert resource-heavy dynamic marketing pages into ultra-fast, cheap-to-host static sites for archival purposes.

Getting started: npm install -g kage

README

main branch

kage

ci Release Go Reference Go Report Card License

kage (影, "shadow") clones a website into a folder you can browse offline, with every script stripped out. It opens each page in real headless Chrome, waits for the page to settle, snapshots the DOM a human would have seen, then deletes all the JavaScript and pulls the CSS, images, and fonts down to local paths. What lands on disk looks like the live site and runs no code.

Install • Quick start • Commands • Clone • Pack • Double-click app • Native window • How it works

kage cloning paulgraham.com, packing it into one file, and serving it back offline

You already know the problem. You hit "Save As" on a page you want to keep, and six months later you open it to find a blank screen, a spinner that never stops, or a copy that still tries to phone home to an analytics server that no longer exists. The page was never really yours. It was a thin client for someone else's JavaScript.

kage takes the other road. It drives a real browser, lets the page finish doing whatever it does, grabs the finished result, and then rips every script out of it. No tracking, no network calls, no surprises. Just .html files you can open straight off disk, hand to a friend, or pack into a single file and forget about for a decade.

Full docs and guides live at kage.tamnd.com.

Install

go install github.com/tamnd/kage/cmd/kage@latest

Prefer a prebuilt binary? Grab an archive, a .deb/.rpm/.apk, or a checksum from releases. Or let a package manager handle it:

# Homebrew (macOS)
brew install --cask tamnd/tap/kage

# Scoop (Windows)
scoop bucket add tamnd https://github.com/tamnd/scoop-bucket
scoop install kage

# apt (Debian, Ubuntu)
curl -fsSL https://tamnd.github.io/linux-repo/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/tamnd.gpg
echo "deb [signed-by=/usr/share/keyrings/tamnd.gpg] https://tamnd.github.io/linux-repo/apt stable main" | sudo tee /etc/apt/sources.list.d/tamnd.list
sudo apt update && sudo apt install kage

# dnf (Fedora, RHEL)
sudo dnf config-manager --add-repo https://tamnd.github.io/linux-repo/dnf/tamnd.repo
sudo dnf install kage

Or skip installing Chrome yourself and use the container image, which bundles Chromium:

docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage clone paulgraham.com

kage drives a real browser, so it needs Chrome or Chromium on the host. It finds a system install on its own; point it somewhere specific with --chrome or the KAGE_CHROME environment variable. The container needs nothing extra.

Shell completion ships in the box: kage completion bash|zsh|fish|powershell.

Quick start

Let's mirror Paul Graham's essays so you can read them on a plane, on a laptop with no wifi, or in the year 2050 after the site has finally changed its design:

# 1. Clone the site into $HOME/data/kage/paulgraham.com/
kage clone paulgraham.com

# 2. Read it back offline in your browser
kage serve $HOME/data/kage/paulgraham.com
# open http://127.0.0.1:8800

That's the whole loop. Every essay, every image, every stylesheet, frozen on your disk and runnable with zero network. The next two steps are optional but nice: collapse the whole thing into one file, and pop it open in its own window.

# 3. Squeeze the mirror into a single shareable file
kage pack paulgraham.com               # -> paulgraham.com.zim
kage open paulgraham.com.zim

# 4. Or into one executable that *is* the site
kage pack paulgraham.com --format binary -o paulgraham
./paulgraham                           # serves itself, needs nothing installed

Commands

Command What it does
kage clone <url> render a site in headless Chrome and write a browsable, script-free mirror
kage serve [dir] preview a cloned folder over a local HTTP server
kage pack <mirror-dir> collapse a mirror into one ZIM archive, a self-contained viewer binary, or a double-click app
kage open <file.zim> serve a packed ZIM back for offline reading

Clone

# The whole site, into $HOME/data/kage/<host>/
kage clone https://paulgraham.com

# Just the first 50 pages, two links deep, for a quick taste
kage clone paulgraham.com --max-pages 50 --max-depth 2

# Only one section of a bigger site
kage clone go.dev --scope-prefix /doc

# Pull in subdomains too, and scroll each page to trip lazy-loaded images
kage clone example.com --subdomains --scroll

# Come back next month and re-render in place to catch new essays
kage clone paulgraham.com --refresh

A clone is a polite, breadth-first crawl. It reads robots.txt, seeds itself from sitemap.xml, and stays on the seed host unless you tell it otherwise. It is also stubbornly idempotent: each page is keyed by the file it writes, so the same essay reached over http and https, with or without a trailing slash, gets fetched exactly once. Hit Ctrl-C and it saves its place on the way out; run it again and it picks up where it stopped. --refresh re-renders in place, --force wipes the host and starts clean.

The flags you'll actually reach for:

Flag Default Meaning
-o, --out $HOME/data/kage Output root; the mirror lands in <out>/<host>/
-p, --max-pages 0 Queue at most N page URLs (0 = no limit); pages that fail, are disallowed, or are not HTML still count
-d, --max-depth 0 How many links deep to follow (0 = no limit)
--scope-prefix Only crawl this path and its descendants, not similar path names
--subdomains false Treat subdomains of the seed host as in scope
--exclude Paths and their descendants to skip (repeatable), not matching substrings elsewhere
--scroll false Auto-scroll each page to trigger lazy loading
--workers 4 How many pages to render at once
--no-robots false Ignore robots.txt (be nice)
--crawl-delay 0s Override robots.txt Crawl-delay between page starts
-f, --force false Delete any existing mirror for the host first
--chrome Path to the Chrome/Chromium binary

kage clone --help has the rest, including render-timing, concurrency, and asset-size knobs.

Serve

kage serve runs a tiny static file server over a cloned folder so links and assets resolve the way they would on a real host:

kage serve $HOME/data/kage/paulgraham.com
# open http://127.0.0.1:8800

Pack it into one file

A mirror is a folder, which is great for browsing and lousy for moving around. Copying thousands of little files is slow, and "here, have this directory" is a clumsy thing to hand someone. kage pack collapses the whole mirror into one artifact, and you choose the shape: an open ZIM archive, or a single executable that is the site.

A single ZIM file

kage pack paulgraham.com               # -> paulgraham.com.zim
kage open paulgraham.com.zim

ZIM is an open file format built for exactly this: a whole website (or a whole Wikipedia) squeezed into one compressed, indexed, read-only file. kage writes the entire mirror into it, text zstd-compressed and media stored as-is. It is the format behind Kiwix, the offline-content project people use to carry Wikipedia, Stack Overflow, and Project Gutenberg onto boats, into classrooms with no internet, and onto a phone for a long flight. Because the format is a documented standard and not a kage invention, a paulgraham.com.zim you make today will still open in any ZIM reader years from now.

So you are not locked into kage. kage open is the quickest way back in, but the very same file works across the wider Kiwix ecosystem:

kage open paulgraham.com.zim            # read it back with kage
kiwix-serve paulgraham.com.zim          # or serve it with Kiwix at http://localhost

You can also double-click the file in the Kiwix desktop app, or load it on Kiwix for Android or iOS to read your mirror on your phone. One caveat: kage writes a structurally valid archive with the standard metadata, but it does not build the full-text search index that Kiwix's own packs ship with, so browsing and clicking work everywhere while in-reader search is limited.

Packing is deterministic. The same mirror always produces a byte-identical file, with the archive UUID derived from the content instead of randomized, so a pack is safe to checksum and cache. A bare host name resolves against the default output directory, which is why kage pack paulgraham.com just works right after kage clone paulgraham.com.

A self-contained binary

--format binary glues the archive onto a copy of kage and hands you a single executable that serves the site offline when you run it. Whoever you send it to needs nothing installed: not kage, not a ZIM reader, nothing.

kage pack paulgraham.com --format binary -o paulgraham
./paulgraham

The appended archive is platform-independent; only the base executable carries the architecture. By default kage appends to itself, so you get a viewer for the machine you ran it on. Point --base at a kage built for another OS (grab one from a release; every platform ships one) to produce a viewer for that platform from your own machine. kage reads the base's executable header to figure out the target, so a Windows viewer automatically gets a .exe name:

# Sitting on a Mac, build a Windows viewer
kage pack paulgraham.com --format binary --base kage-windows-amd64.exe   # -> paulgraham.exe

The trade is size. The binary carries a whole kage, so it weighs around 13 MiB plus the site no matter how small the mirror is. When you only need the content, the ZIM is far leaner.

A double-click app

A bare binary is great from a terminal, but double-click it in a file manager and the experience is rough: macOS opens a Terminal window behind the site, and on Windows a console flashes up next to it. Add --app and kage wraps the same viewer in a proper desktop app so a double-click just opens the site, no terminal, with the mirror's own favicon as the icon.

On macOS you get a real .app bundle:

kage pack paulgraham.com --app                 # -> paulgraham.app
open paulgraham.app                            # or double-click it in Finder

On Linux, point --base at a Linux kage and you get an AppImage-style .AppDir with a .desktop launcher (Terminal=false, so no console). If appimagetool is installed, kage folds it into a single double-clickable .AppImage for you:

kage pack paulgraham.com --app --base kage-linux-amd64   # -> paulgraham.AppDir (+ .AppImage)

kage finds the icon by digging the favicon out of the mirror (it prefers a large apple-touch-icon.png and falls back to favicon.ico); pass --icon some.png to override it. Pair --app with a webview base (below) and the double-click opens a native window instead of the browser, which is the full "it's an app" effect.

Windows needs no bundle, because there a single .exe already is the app. The catch is the console window. The release ships a kage_<version>_windows-gui_<arch>.zip whose binary is linked for the GUI subsystem, so a viewer packed onto it opens with no console behind it:

# Build a console-free Windows viewer (from any OS)
kage pack paulgraham.com --format binary --base kage-windows-gui-amd64.exe   # -> paulgraham.exe

A real window, not a browser tab

By default a packed binary opens your system browser, which means the site shows up as yet another tab, address bar and all, next to the 47 you already have open. Build kage with the webview tag and it opens the site in its own window instead, backed by the operating system's WebView (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux). Paul Graham's essays, offline, in something that looks and feels like a real app:

paulgraham.com served offline in a native kage window

make build-webview                       # or: CGO_ENABLED=1 go build -tags webview ./cmd/kage
kage pack paulgraham.com --format binary --base bin/kage -o paulgraham
./paulgraham                             # opens a window, no browser in sight

This build needs cgo and links the platform WebView, so it stays opt-in. The default build is pure Go (CGO_ENABLED=0) and the prebuilt release binaries open the browser, which keeps the cross-compiled release simple. kage open honours the same tag, so built with -tags webview it shows a ZIM in a native window too.

How it works

seed URL ─▶ headless Chrome ─▶ final DOM ─▶ strip JS ─▶ localise assets ─▶ disk
              (render)          (snapshot)   (sanitize)   (rewrite links)

A pool of Chrome tabs renders pages; a separate pool fetches assets over plain HTTP. Every URL maps deterministically to a local path, so links get rewritten before the asset they point at has even finished downloading. The output looks like this:

paulgraham.com/
├── index.html                  # the home page, scripts stripped
├── greatwork.html              # /greatwork.html, an essay
├── _kage/                      # reserved: assets and crawl state
│   ├── paulgraham.com/site.css  # localised stylesheet (url() rewritten)
│   ├── paulgraham.com/pg.png
│   └── state.json              # visited set, for resuming
└── ...

pack rides on the same idea: the mirror's links are already mirror-relative paths, and those map one-to-one onto the archive's content entries, so a click in a served page hits the right entry with no rewriting at all.

Building from source

git clone https://github.com/tamnd/kage
cd kage
make build          # -> bin/kage (pure Go, opens the browser)
make build-webview  # -> bin/kage with the native-window viewer (needs cgo)
make test           # full suite, including the Chrome-driven end-to-end tests
make test-short     # skip the tests that launch a browser

The repo is split by concern:

cmd/kage/   thin main: pins the main thread, then hands off to cli.Execute
cli/        the cobra command tree and flag wiring
clone/      the crawl: frontier, render workers, asset workers, resume state
browser/    headless Chrome control and DOM snapshotting
sanitize/   strip scripts, handlers, and javascript: URLs from the DOM
asset/      download and localise CSS, images, and fonts
urlx/       the deterministic URL-to-path mapping
zim/        a pure-Go ZIM reader and writer
pack/       mirror to ZIM or self-contained binary, and the offline HTTP handler
viewer/     present a served site: system browser, or native window (webview tag)
docs/       the tago documentation site

Releasing

Push a version tag and GitHub Actions runs GoReleaser, which builds the archives, the .deb/.rpm/.apk packages, a multi-arch GHCR image with Chromium bundled, checksums, SBOMs, and a cosign signature:

git tag v0.1.1
git push --tags

The image tag carries no v prefix (ghcr.io/tamnd/kage:0.1.1). The Homebrew and Scoop steps self-disable until their tokens exist, so the first release works with no extra secrets.

License

MIT. See LICENSE.

View on GitHub

Recent activity

commits and pull requests

Recent open issues

view all

Releases and announcements

18 total
  1. v0.3.12v0.3.12Aug 10, 20264.4K downloads

    The largest community release kage has had. Most of what is below arrived as pull requests from other people, and the rest came from bug reports that were specific enough to act on. - **`--resume` actually resumes.** `state.json` only ever persisted the visited set, so a resumed run found its seed already visited, queued nothing, printed `pages 0` and exited successfully with most of the site still missing. The unfinished frontier is now saved next to the visited set, with each page's depth so `--max-depth` keeps its meaning across a restart, and a run reports what it is leaving behind. A page that failed is carried into the next run instead of being lost, which is the memory of what failed asked for in [#36](https://github.com/tamnd/kage/issues/36). `--max-pages` no longer throws away the pages it held back, so inspecting a site with `-p 20` and then finishing it later works as a workflow. - **Saved pages keep their doctype.** kage serialises a rendered page as the outerHTML of `<html>`, and a doctype is a sibling of `<html>` rather than a child, so every page kage had ever written came out without one. A document with no doctype is quirks mode in every browser, which changes th

  2. v0.3.11v0.3.11Aug 1, 2026804 downloads

    ## Changelog ### Other * 9d7abe15ba134ba318d30bd92406475fec726feb: Restore versioned Go installs without leakless (@tamnd) * 25a6ebe47ce03d2d104d7a89664237ef1f4e00fd: Update golang.org/x/text to v0.39.0 for GO-2026-5970 (@tamnd)

  3. v0.3.10v0.3.10Jul 11, 20261.5K downloads

    ## Changelog ### Other * 1ba8c8943e18b8096a33fa283351f36e93638d4a: Bump go toolchain to 1.26.5 (#71) (@tamnd)

  4. v0.3.9v0.3.9Jul 8, 2026456 downloads

    ## Changelog ### Other * 7483efd2a9141bea17238724f7e9f65976e5f419: Dequarantine the brew cask binary on install (#67) (@tamnd) * 35860d5aaa9cee9787b1dc35a955592fffadad54: Drop the embedded leakless binary that trips antivirus (#69) (@tamnd) * 320b21a2ba2f30ff8f3c128098ca9d8c1cf3e6aa: Pin release actions and cosign to fixed versions (#63) (@tamnd) * 2dabb93a78efe8b0762c13ad273a8d0a74920174: Run the container as root so a bind-mounted /out works (issue #7) (#53) (@tamnd)

  5. v0.3.8v0.3.8Jun 22, 20261.7K downloads

    ## What's changed **Fix: Windows binary no longer blocked by Defender** kage used go-rod's leakless launcher helper to guard Chrome processes, and that helper binary (`leakless.exe`) was being flagged as a trojan by Windows Defender (false positive, issues #7, #42). The fix disables leakless on Windows while leaving the Chrome cleanup path unchanged on all other platforms. The Windows binary now installs and runs cleanly with no AV interference. **Fix: brew install command now includes --cask** The README install snippet was missing `--cask`, so `brew install kage` would fail on a fresh machine. Fixed in the docs. **Feature: respect Crawl-delay from robots.txt during clone** When a site's `robots.txt` sets a `Crawl-delay` directive, kage now honors it between page fetches. This is a first step toward full crawl-rate control (#6); a `--crawl-delay` flag to override it from the command line is coming in a follow-up. ## Upgrade ```sh # Homebrew brew upgrade kage # Scoop scoop update kage # Docker docker pull ghcr.io/tamnd/kage:0.3.8 ```

Code frequency

additions and deletions
+13.5K-13.5KWeek of 2026-06-14: +13,464 linesWeek of 2026-06-14: -815 linesWeek of 2026-06-21: +118 linesWeek of 2026-06-21: -20 linesWeek of 2026-06-28: +2 linesWeek of 2026-06-28: -2 linesWeek of 2026-07-05: +93 linesWeek of 2026-07-05: -5 linesWeek of 2026-07-12: +0 linesWeek of 2026-07-12: -0 linesWeek of 2026-07-19: +0 linesWeek of 2026-07-19: -0 linesWeek of 2026-07-26: +6,791 linesWeek of 2026-07-26: -88 linesWeek of 2026-08-02: +973 linesWeek of 2026-08-02: -50 linesWeek of 2026-08-09: +951 linesWeek of 2026-08-09: -111 linesWeek of 2026-08-16: +0 linesWeek of 2026-08-16: -0 linesJun 14, 2026Aug 16, 2026
+22.4K lines added, -1.1K removed over the last year.

Commits per week

last 52 weeks
610Week of 2025-09-28: 0 commitsWeek of 2025-10-05: 0 commitsWeek of 2025-10-12: 0 commitsWeek of 2025-10-19: 0 commitsWeek of 2025-10-26: 0 commitsWeek of 2025-11-02: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 0 commitsWeek of 2026-02-08: 0 commitsWeek of 2026-02-15: 0 commitsWeek of 2026-02-22: 0 commitsWeek of 2026-03-01: 0 commitsWeek of 2026-03-08: 0 commitsWeek of 2026-03-15: 0 commitsWeek of 2026-03-22: 0 commitsWeek of 2026-03-29: 0 commitsWeek of 2026-04-05: 0 commitsWeek of 2026-04-12: 0 commitsWeek of 2026-04-19: 0 commitsWeek of 2026-04-26: 0 commitsWeek of 2026-05-03: 0 commitsWeek of 2026-05-10: 0 commitsWeek of 2026-05-17: 0 commitsWeek of 2026-05-24: 0 commitsWeek of 2026-05-31: 0 commitsWeek of 2026-06-07: 0 commitsWeek of 2026-06-14: 61 commitsWeek of 2026-06-21: 3 commitsWeek of 2026-06-28: 1 commitsWeek of 2026-07-05: 3 commitsWeek of 2026-07-12: 0 commitsWeek of 2026-07-19: 0 commitsWeek of 2026-07-26: 2 commitsWeek of 2026-08-02: 4 commitsWeek of 2026-08-09: 12 commitsWeek of 2026-08-16: 0 commitsWeek of 2026-08-23: 0 commitsWeek of 2026-08-30: 0 commitsWeek of 2026-09-06: 0 commitsWeek of 2026-09-13: 0 commitsWeek of 2026-09-20: 0 commitsSep 28, 2025Sep 20, 2026
86 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 0 commitsSun 1:00 — 0 commitsSun 2:00 — 0 commitsSun 3:00 — 0 commitsSun 4:00 — 0 commitsSun 5:00 — 0 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 0 commitsSun 9:00 — 0 commitsSun 10:00 — 1 commitsSun 11:00 — 0 commitsSun 12:00 — 0 commitsSun 13:00 — 0 commitsSun 14:00 — 0 commitsSun 15:00 — 0 commitsSun 16:00 — 0 commitsSun 17:00 — 0 commitsSun 18:00 — 3 commitsSun 19:00 — 2 commitsSun 20:00 — 4 commitsSun 21:00 — 1 commitsSun 22:00 — 1 commitsSun 23:00 — 0 commitsMon 0:00 — 9 commitsMon 1:00 — 1 commitsMon 2:00 — 0 commitsMon 3:00 — 0 commitsMon 4:00 — 0 commitsMon 5:00 — 0 commitsMon 6:00 — 0 commitsMon 7:00 — 0 commitsMon 8:00 — 0 commitsMon 9:00 — 0 commitsMon 10:00 — 0 commitsMon 11:00 — 0 commitsMon 12:00 — 6 commitsMon 13:00 — 2 commitsMon 14:00 — 11 commitsMon 15:00 — 1 commitsMon 16:00 — 1 commitsMon 17:00 — 0 commitsMon 18:00 — 1 commitsMon 19:00 — 5 commitsMon 20:00 — 1 commitsMon 21:00 — 2 commitsMon 22:00 — 1 commitsMon 23:00 — 1 commitsTue 0:00 — 2 commitsTue 1:00 — 0 commitsTue 2:00 — 0 commitsTue 3:00 — 0 commitsTue 4:00 — 0 commitsTue 5:00 — 0 commitsTue 6:00 — 0 commitsTue 7:00 — 0 commitsTue 8:00 — 1 commitsTue 9:00 — 1 commitsTue 10:00 — 1 commitsTue 11:00 — 0 commitsTue 12:00 — 0 commitsTue 13:00 — 0 commitsTue 14:00 — 0 commitsTue 15:00 — 2 commitsTue 16:00 — 0 commitsTue 17:00 — 0 commitsTue 18:00 — 0 commitsTue 19:00 — 0 commitsTue 20:00 — 0 commitsTue 21:00 — 0 commitsTue 22:00 — 0 commitsTue 23:00 — 1 commitsWed 0:00 — 0 commitsWed 1:00 — 0 commitsWed 2:00 — 0 commitsWed 3:00 — 0 commitsWed 4:00 — 0 commitsWed 5:00 — 0 commitsWed 6:00 — 0 commitsWed 7:00 — 0 commitsWed 8:00 — 1 commitsWed 9:00 — 0 commitsWed 10:00 — 0 commitsWed 11:00 — 0 commitsWed 12:00 — 0 commitsWed 13:00 — 0 commitsWed 14:00 — 0 commitsWed 15:00 — 0 commitsWed 16:00 — 1 commitsWed 17:00 — 0 commitsWed 18:00 — 1 commitsWed 19:00 — 1 commitsWed 20:00 — 0 commitsWed 21:00 — 0 commitsWed 22:00 — 0 commitsWed 23:00 — 0 commitsThu 0:00 — 0 commitsThu 1:00 — 0 commitsThu 2:00 — 0 commitsThu 3:00 — 0 commitsThu 4:00 — 0 commitsThu 5:00 — 0 commitsThu 6:00 — 0 commitsThu 7:00 — 0 commitsThu 8:00 — 0 commitsThu 9:00 — 0 commitsThu 10:00 — 2 commitsThu 11:00 — 1 commitsThu 12:00 — 0 commitsThu 13:00 — 0 commitsThu 14:00 — 0 commitsThu 15:00 — 1 commitsThu 16:00 — 0 commitsThu 17:00 — 0 commitsThu 18:00 — 0 commitsThu 19:00 — 0 commitsThu 20:00 — 0 commitsThu 21:00 — 0 commitsThu 22:00 — 0 commitsThu 23:00 — 0 commitsFri 0:00 — 0 commitsFri 1:00 — 0 commitsFri 2:00 — 0 commitsFri 3:00 — 0 commitsFri 4:00 — 0 commitsFri 5:00 — 0 commitsFri 6:00 — 0 commitsFri 7:00 — 0 commitsFri 8:00 — 0 commitsFri 9:00 — 0 commitsFri 10:00 — 0 commitsFri 11:00 — 0 commitsFri 12:00 — 0 commitsFri 13:00 — 0 commitsFri 14:00 — 0 commitsFri 15:00 — 2 commitsFri 16:00 — 3 commitsFri 17:00 — 1 commitsFri 18:00 — 0 commitsFri 19:00 — 2 commitsFri 20:00 — 0 commitsFri 21:00 — 0 commitsFri 22:00 — 2 commitsFri 23:00 — 3 commitsSat 0:00 — 0 commitsSat 1:00 — 0 commitsSat 2:00 — 0 commitsSat 3:00 — 0 commitsSat 4:00 — 0 commitsSat 5:00 — 0 commitsSat 6:00 — 0 commitsSat 7:00 — 0 commitsSat 8:00 — 0 commitsSat 9:00 — 1 commitsSat 10:00 — 1 commitsSat 11:00 — 0 commitsSat 12:00 — 0 commitsSat 13:00 — 0 commitsSat 14:00 — 0 commitsSat 15:00 — 0 commitsSat 16:00 — 0 commitsSat 17:00 — 0 commitsSat 18:00 — 0 commitsSat 19:00 — 0 commitsSat 20:00 — 0 commitsSat 21:00 — 0 commitsSat 22:00 — 0 commitsSat 23:00 — 1 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.

Who is committing

last 52 weeks
Maintainer commits98 (88%)
Community commits13 (12%)

111 commits in total over the last year.

DateListRankStars gained
Jun 15, 2026daily#14+23
Jun 14, 2026daily#21+35
  • avelino/awesome-go

    A curated list of awesome Go frameworks, libraries and software

    186.9K stars · Go

  • kubernetes/kubernetes

    Production-Grade Container Scheduling and Management

    128.3K stars · Go

  • microsoft/TypeScript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

    111.3K stars · Go

  • JuliusBrussee/caveman

    🪨 why use many token when few token do trick. Viral skill + proxy for coding agents that cuts 65% of tokens by talking like a caveman.

    109.8K stars · Go

  • infiniflow/ragflow

    RAGFlow is a leading open-source Retrieval-Augmented Generation (RAG) engine that fuses cutting-edge RAG with Agent capabilities to create a superior context layer for LLMs

    91.7K stars · Go

  • caddyserver/caddy

    Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

    76.5K stars · Go