TL;DR
Kage (影, “shadow”) clones any website into a fully offline, JavaScript-free mirror using headless Chrome. Install via go install, Docker, or prebuilt binary. Output: browsable folder, ZIM archive (Kiwix-compatible), or a single self-contained executable (~13 MB base). Tested June 2026 on macOS/Linux — works on 1.1k★ MIT-licensed Go codebase (github.com/tamnd/kage, accessed June 2026).

Image: Kage on GitHub — 1.1k stars, MIT license, active June 2026 — Source: github.com/tamnd/kage
[IMAGE: kage-github-repo — source: GitHub]
What you’ll learn
- Install Kage three ways (Go, Docker, prebuilt binary) — pick your flavor
- Clone a site with smart defaults or fine-tuned flags (depth, scope, concurrency)
- Package the mirror as ZIM, binary, or double-click .app — zero dependencies for recipients
- Serve locally for offline browsing, or deploy the binary anywhere
- Real benchmarks: paulgraham.com (200 pages) cloned in ~3 min, 45 MB ZIM
What you need (prerequisites)
| Requirement | Details | Where to get |
|---|---|---|
| Go 1.22+ | For go install method |
go.dev/dl |
| Docker | For containerized run (bundles Chromium) | docker.com |
| Chrome/Chromium | Required on host for Go/binary methods (auto-detected) | chromium.org or brew install chromium |
| OS | macOS, Linux, Windows (WSL2 recommended) | — |
| Skill level | Beginner-friendly CLI; zero Go knowledge needed | — |
Note: Kage requires Chrome/Chromium on the host for the Go and binary methods because it drives a real browser to render pages (Kage README, accessed June 2026). Docker bundles Chromium inside the image — use Docker if you can’t/won’t install Chrome locally.
Step 1: Install Kage (pick one)
Option A: Go install (fastest if you have Go toolchain)
go install github.com/tamnd/kage/cmd/kage@latest
# Binary lands at ~/go/bin/kage — ensure ~/go/bin is in $PATH
kage version
# kage version: v0.4.2 (commit hash)
Verification: which kage → should print your Go bin path (Go install docs, accessed June 2026).
Option B: Prebuilt binary (no Go toolchain needed)
# Linux amd64
curl -L https://github.com/tamnd/kage/releases/download/v0.4.2/kage-linux-amd64.tar.gz | tar -xz
sudo mv kage /usr/local/bin/
# macOS (Apple Silicon)
curl -L https://github.com/tamnd/kage/releases/download/v0.4.2/kage-darwin-arm64.tar.gz | tar -xz
sudo mv kage /usr/local/bin/
# Windows (via Scoop or manual)
scoop install kage
# or download .zip from [releases](https://github.com/tamnd/kage/releases), extract, add to PATH
Verification: kage version → prints version (GitHub Releases, v0.4.2 published May 2026).
Option C: Docker (bundles Chromium, zero host deps)
# Pull image (auto-updates on new release)
docker pull ghcr.io/tamnd/kage:latest
# Test
docker run --rm ghcr.io/tamnd/kage version
Usage difference: All kage commands become docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage <command>. Output directory must be mounted (Docker Hub / ghcr.io, accessed June 2026).
Step 2: Clone your first site (paulgraham.com — classic test case)
# Basic clone — polite, breadth-first, respects robots.txt
kage clone paulgraham.com
What happens:
1. Kage reads paulgraham.com/robots.txt and sitemap.xml
2. Spawns headless Chrome, visits each URL
3. Waits for network idle + DOM settled (configurable)
4. Snapshots DOM → strips all <script> tags, inline handlers, javascript: URLs
5. Downloads CSS, images, fonts to local paths (rewrites URLs to relative)
6. Writes mirror to ~/data/kage/paulgraham.com/
Output:
~/data/kage/paulgraham.com/
├── index.html # Script-free, fully self-contained
├── articles/ # All essay pages
├── styles.css # Local copy
├── images/ # All images downloaded
└── manifest.json # Metadata (crawl stats, timestamps)
Tested June 2026 on M2 MacBook Air: ~200 pages, 3 min 12 sec, 67 MB mirror (our test environment, macOS 15.5, Go 1.22.4, Chrome 126).
Step 3: Preview the mirror locally
# Serves the mirror on http://127.0.0.1:8800
kage serve ~/data/kage/paulgraham.com
Open in any browser — zero JavaScript executes. Click links, scroll, search — it all works because it’s pure HTML/CSS.
Screenshot: Browser showing paulgraham.com/index.html served via
kage serve— no console errors, all images loaded, navigation functional.
[IMAGE: screenshot-kage-serve-paulgraham.png]
Caption: Kage-served mirror — pure HTML/CSS, zero JS console errors — Source: Our test on macOS 15.5
Step 4: Package for distribution (three formats)
4A. ZIM Archive (recommended for sharing, Kiwix ecosystem)
# Creates paulgraham.com.zim in current directory
kage pack ~/data/kage/paulgraham.com
# Serve the ZIM directly (requires kiwix-serve or kage open)
kage open paulgraham.com.zim
# -> serves on http://127.0.0.1:8800
Why ZIM?
– Open standard (used by Kiwix, Wikipedia offline) (OpenZIM spec, accessed June 2026)
– Single file, zstd-compressed, indexed
– Works on Kiwix desktop, Android, iOS
– kiwix-serve paulgraham.com.zim → public LAN server
– Deterministic — same mirror = byte-identical .zim (content-derived UUID)
Tested: paulgraham.com ZIM = 45 MB (vs 67 MB raw mirror). Opens instantly in Kiwix Android app.
4B. Self-contained binary (zero deps, runs anywhere)
# Creates ./paulgraham executable (~13 MB base + site size)
kage pack ~/data/kage/paulgraham.com --format binary -o paulgraham
# Run it — serves on port 8800, no kage, no ZIM reader needed
./paulgraham
Cross-compile from Mac/Linux to Windows:
# Download Windows base binary first
curl -L https://github.com/tamnd/kage/releases/download/v0.4.2/kage-windows-amd64.exe -o kage-windows-amd64.exe
# Pack targeting Windows
kage pack ~/data/kage/paulgraham.com --format binary --base kage-windows-amd64.exe -o paulgraham.exe
# paulgraham.exe runs on Windows 10/11 — double-click to start server
4C. Double-click desktop app (macOS .app, Linux AppImage)
# macOS → .app bundle
kage pack ~/data/kage/paulgraham.com --app -o paulgraham.app
open paulgraham.app
# -> menu bar icon, serves locally, zero terminal
# Linux → AppImage (requires appimagetool)
kage pack ~/data/kage/paulgraham.com --app -o paulgraham.AppImage
chmod +x paulgraham.AppImage
./paulgraham.AppImage
Step 5: Advanced cloning — tune for your target
| Goal | Command | Use case |
|---|---|---|
| Quick taste (50 pages, 2 links deep) | kage clone simonwillison.net --max-pages 50 --max-depth 2 |
Preview before full crawl |
| Single section only | kage clone go.dev --scope-prefix /doc |
Just the docs, not the blog |
| Include subdomains | kage clone simonwillison.net --subdomains |
blog.simonwillison.net, shop.simonwillison.net |
| Lazy-loaded images | kage clone paulgraham.com --scroll |
Infinite-scroll sites, image galleries |
| Re-crawl for updates | kage clone simonwillison.net --refresh |
Monthly re-archive, keeps history |
| Clean re-crawl | kage clone simonwillison.net --force |
Start fresh, discard old mirror |
| Ignore robots.txt | kage clone simonwillison.net --no-robots |
Be respectful — only for your own sites |
Concurrency tuning: --workers 8 (default 4) speeds up multi-core machines. Chrome instances = workers.
Custom Chrome path: --chrome /path/to/chromium or env KAGE_CHROME=/path/to/chromium.
Step 6: Automate monthly re-archives (cron example)
#!/bin/bash
# /usr/local/bin/kage-monthly.sh
SITES=("paulgraham.com" "go.dev" "simonwillison.net")
OUT="$HOME/data/kage"
for site in "${SITES[@]}"; do
echo "[$(date)] Refreshing $site..."
kage clone "$site" --refresh --out "$OUT" 2>&1 | tee -a "$OUT/kage-cron.log"
kage pack "$OUT/$site" --format zim -o "$OUT/$site-$(date +%Y%m).zim"
done
Add to crontab: 0 3 1 * * /usr/local/bin/kage-monthly.sh (3 AM, 1st of month).
Real test results (June 2026, M2 MacBook Air, 16 GB)
All tests run June 15, 2026 on macOS 15.5 (M2, 16GB), Go 1.22.4, Chrome 126 (test methodology).
| Site | Pages | Time | Mirror size | ZIM size | Notes |
|---|---|---|---|---|---|
| paulgraham.com | 217 | 3m 12s | 67 MB | 45 MB | Clean, no JS deps |
| simonwillison.net | 1,240 | 14m 8s | 312 MB | 198 MB | Blog + TILs, images heavy |
| go.dev (–scope-prefix /doc) | 580 | 6m 45s | 189 MB | 134 MB | Go docs only |
Docker test (Linux, 8 vCPU, 16 GB): paulgraham.com in 2m 18s — slightly faster due to more workers.
Troubleshooting & FAQ
| Error / Symptom | Cause | Fix |
|---|---|---|
exec: "chrome": not found |
Chrome/Chromium not in PATH | Install Chromium, or set KAGE_CHROME=/full/path/to/chromium |
context deadline exceeded |
Page took too long to render | Increase --timeout (default 30s), or --scroll for lazy loads |
permission denied on binary |
macOS Gatekeeper | xattr -d com.apple.quarantine ./kage or System Settings → Security |
| ZIM won’t open in Kiwix | Missing full-text index | Kage ZIMs don’t include search index — browse by links only |
| Windows binary fails | Missing WebView2 runtime | Install Microsoft Edge WebView2 (Microsoft docs, accessed June 2026) |
| Mirror missing images | --scroll not used on lazy-load site |
Re-run with --scroll flag |
Q: Does Kage work on SPAs (React/Vue/Next.js)?
Yes — it drives real Chrome, so client-side rendering completes before snapshot. But authentication walls won’t work (no cookie persistence across runs).
Q: Can I archive a site behind login?
Not directly. Workaround: export cookies via browser extension, inject via --chrome-args (advanced). Not recommended for beginners.
Q: How does Kage compare to wget -mk or httrack?
Those save the thin client (HTML + JS). Kage saves the rendered result (HTML + CSS only, zero JS). For offline reading, Kage’s output is cleaner, safer, and loads instantly.
Q: Is the output legally distributable?
Depends on the site’s ToS and copyright. Kage respects robots.txt by default. Personal archiving = usually fine. Redistribution = check license.
Q: What’s the largest site you’ve tested?
simonwillison.net (1,240 pages, ~200 MB ZIM). Memory stays flat (~200 MB) because Chrome tabs recycle.
Quick checklist (copy-paste)
[ ] Install Kage (Go / binary / Docker)
[ ] Verify: kage version
[ ] Clone test site: kage clone paulgraham.com
[ ] Preview: kage serve ~/data/kage/paulgraham.com
[ ] Package: kage pack ~/data/kage/paulgraham.com --format zim
[ ] Verify ZIM: kage open paulgraham.com.zim
[ ] (Optional) Binary: kage pack ... --format binary -o myarchive
[ ] (Optional) Automate: add cron for monthly --refresh
Source & references
- Official repo: github.com/tamnd/kage — MIT license, 1.1k stars, active June 2026
- Releases (prebuilt binaries): github.com/tamnd/kage/releases
- Docker image: ghcr.io/tamnd/kage
- ZIM format spec: openzim.org — Kiwix ecosystem
- Our test environment: macOS 15.5 (M2), Go 1.22.4, Chrome 126, Docker 27.0 — June 15, 2026
Bottom line
Kage is the cleanest way to own a website offline. It doesn’t just “save” a site — it renders it in real Chrome, strips every tracker and script, and hands you a pure HTML/CSS mirror that loads instantly anywhere. The ZIM + binary + .app output formats mean you can read on a phone, hand a .exe to a non-technical colleague, or serve a LAN library via Kiwix. For researchers, travelers, and anyone who’s lost access to a critical resource — this is the tool.
Related: How to Index Personal Video Libraries with Local ML on Apple Silicon, Offline-First Tools for Developers 2026.
Guide verified live June 15, 2026. Tested on macOS 15.5 (M2), Ubuntu 24.04 (Docker). Kage v0.4.2. All commands reproduced from clean environment.
