npx screenshot-time-machine@latest # the app you have running on localhost
npx screenshot-time-machine@latest --url https://your-site.com # or any site you are allowed to capture
npx skills add mdsohaib/screenshot-time-machine # teach Claude Code, Cursor, Codex and friends to run it
The one command
You are building. Your app is running on localhost. You type three letters:
$ stm
7 pages saved → screenshots/localhost-3000_2026-08-15_14-32/
Run stm again after your next change to see which pages look different.
Seven full-page screenshots, top to bottom, are now sitting in a dated folder next to your code. Open it in Finder or Explorer and your entire site is right there as a contact sheet: home, pricing, about, every product page, the whole thing, exactly as it looks today.
Already live? Point it at the real thing and it works the same way, reading your sitemap.xml to find the pages:
$ stm --url https://your-site.com
24 pages saved → screenshots/your-site-com_2026-08-15_14-40/
That is the tool. Everything below is what that simple thing turns into once you have run it a few times.
What you get
Your whole site in one folder. Not the one page you happen to have open. Every page, full length, top to bottom, in a folder you can scroll through in ten seconds. It is the fastest way to answer "how does my site actually look right now".
The pages you would never have opened. Padding that collapsed on the pricing page. A heading that wrapped badly. A card that lost its image. A footer that drifted. It is all sitting in the same folder waiting to be noticed, instead of waiting to be reported.
A history that builds itself. Every run is its own dated folder, so they stack into a timeline on their own. Last month's home page and today's home page are two files you can flip between. Six months in, you have proof of how far the thing came.
Phone views in the same run. stm --mobile also captures a 390x844 view of every page, so the heading that wraps badly on a phone lands right next to the desktop shot.
A shortlist instead of forty tabs. From the second run onward, stm prints the pages whose screenshots came out different from last time. Start there.
Eyes for your agent. stm --json hands Claude Code, Cursor or Codex the absolute path of every page it just built, so it can look at its own work and fix it before telling you it is done.
Yours, permanently. Plain PNG files on your own disk. No account, no cloud, no subscription, no config file, no background process. One dependency, MIT licensed, yours forever.
Who reaches for it
- You prompt, the agent builds, and you find out later. You asked for one page, it edited a shared component, and eleven pages came along for the ride. One command shows you all eleven.
- You are shipping alone at 1am. The design pass felt right in the moment. The folder from Tuesday tells you whether it actually was.
- You are three weeks into a redesign and cannot quite remember what it used to look like. Capture the site on day one and every screenshot after that is a comparison you never had to set up.
- You are handing work over. A folder of full-page screenshots is the clearest status update a client or a teammate will ever get.
- You want proof of the work. Version one of anything is worth keeping. Most people work that out around version four.
Quick start
If you are building locally, start your app the way you normally do (npm run dev, rails s, python manage.py runserver, a static server, anything that serves HTML) and run:
npx screenshot-time-machine@latest
If the site is already online, skip all of that and name it:
npx screenshot-time-machine@latest --url https://your-site.com
Needs Node 22 or newer. Run node -v to check. If it prints 21 or lower, update Node from nodejs.org first.
For screenshots, stm uses a Playwright browser you already have cached, or failing that the Chrome or Edge already on your machine. If you have neither, it downloads a headless browser once, about a 120 MB download that takes roughly 200 MB on disk, and tells you before it does. That is the only thing stm itself fetches from the internet; the pages you capture load their own resources, exactly as they would in your browser.
Change something, run it again. That is the whole workflow.
Everything below writes the command as stm for short. To get that short name, install it once:
npm install -g screenshot-time-machine
Not installing is fine. Write npx screenshot-time-machine@latest wherever you see stm. Never write npx stm: that is an unrelated package from 2014.
stmprobes ports 3000, 3001, 5173, 5174, 8080, 4321, 4322, 8000, 4200 and 5000, in that order, and uses the first app that answers. If several answer, it prefers the one this project used last time and tells you about the others.- If your project has a
.gitignore, it addsscreenshots/to it once and tells you. It never creates a.gitignore, and it never deletes anything. - Pages are found by following the
<a href>links on them and by reading yoursitemap.xmlif you have one, up to 100 pages per run. Pages reached only by a button click, and hash routes like/#/about, have no link to follow, so name those yourself:stm /about /pricing.
Install it in your AI coding agent
Your agent wrote the pricing page. It wrote the nav. It touched a shared component and eleven pages moved with it. It has never actually looked at any of them, because it has no eyes.
stm --json gives it some. The agent runs one command, gets back the absolute path of every page it just built, opens the fold image of the ones that came out different, and checks its own work before it tells you it is done.
The good part is what you stop doing. No clicking through eleven pages. No "looks fine" that turns out not to be. You get a folder, you scroll it in ten seconds, and you already know which pages to open first.
One command installs stm as a skill: a short instruction file your agent reads, so it knows when to run stm and what to do with the result. npx skills add is a separate open-source installer that copies that one folder into your project and changes nothing else. Pick your agent:
| Agent | Command | Skill lands in |
|---|---|---|
| Claude Code | npx skills add mdsohaib/screenshot-time-machine -a claude-code -y |
.claude/skills/stm/ |
| Codex | npx skills add mdsohaib/screenshot-time-machine -a codex -y |
.agents/skills/stm/ |
| Cursor | npx skills add mdsohaib/screenshot-time-machine -a cursor -y |
.agents/skills/stm/ |
| Gemini CLI | npx skills add mdsohaib/screenshot-time-machine -a gemini-cli -y |
.agents/skills/stm/ |
| Antigravity | npx skills add mdsohaib/screenshot-time-machine -a antigravity -y |
.agents/skills/stm/ |
| GitHub Copilot | npx skills add mdsohaib/screenshot-time-machine -a github-copilot -y |
.agents/skills/stm/ |
| Windsurf | npx skills add mdsohaib/screenshot-time-machine -a windsurf -y |
.windsurf/skills/stm/ |
| OpenCode | npx skills add mdsohaib/screenshot-time-machine -a opencode -y |
.agents/skills/stm/ |
| Cline | npx skills add mdsohaib/screenshot-time-machine -a cline -y |
.agents/skills/stm/ |
| Anything else | npx skills add mdsohaib/screenshot-time-machine |
pick your agent from the list below |
Add -g to any of those to install it for every project instead of this one.
Every agent you can install into
These are the names the installer accepts after -a. Pass any one of them, or leave -a off and pick from the menu it shows you.
adal · aider-desk · amp · antigravity · antigravity-cli · astrbot · augment · autohand-code · bob · claude-code · cline · codearts-agent · codebuddy · codemaker · codestudio · codex · command-code · continue · cortex · crush · cursor · deepagents · devin · dexto · droid · eve · firebender · forgecode · gemini-cli · github-copilot · goose · grok · hermes-agent · iflow-cli · inference-sh · jazz · junie · kilo · kimchi · kimi-code-cli · kiro-cli · kode · lingma · loaf · mcpjam · minimax-code · mistral-vibe · moxby · mux · neovate · ona · openclaw · opencode · openhands · pi · pochi · promptscript · qoder · qoder-cn · qwen-code · reasonix · replit · roo · rovodev · tabnine-cli · terramind · tinycloud · trae · trae-cn · universal · warp · windsurf · zcode · zed · zencoder · zenflow
universal is the one to use when your agent is not named above: it writes the skill to .agents/skills/stm/, the shared location several of these agents read. The list lives in the installer rather than here, so it grows without this README changing. To see the current one, ask for an agent that does not exist and it prints them all:
npx skills add mdsohaib/screenshot-time-machine -a list-them-please
Or just tell your agent, in plain words:
Install screenshot-time-machine, run
stmon my app, and show me how the pages look.
Then add one line to the file your agent reads on every request, so it checks its own work without being asked:
After changing anything a user can see, run `stm --json` (or `npx -y screenshot-time-machine@latest --json` if `stm` is not installed) and view the `fold` image of every entry in `changed` and `new` before saying you're done. On the very first snapshot both lists are empty, so view the fold images of the pages you edited instead. Never claim a UI change is verified without viewing a screenshot.
Where that line goes, by agent:
| Agent | File |
|---|---|
| Claude Code | CLAUDE.md |
| Codex, Antigravity, OpenCode | AGENTS.md |
| Gemini CLI | GEMINI.md |
| Cursor | .cursor/rules/stm.mdc, starting with ---, then alwaysApply: true, then --- |
| GitHub Copilot | .github/copilot-instructions.md |
| Windsurf | .windsurf/rules/stm.md |
| Cline | .clinerules/stm.md |
Skills are the simple path. If you prefer plugins, run this inside Claude Code:
/plugin marketplace add mdsohaib/screenshot-time-machine
/plugin install stm@stm
No SSH key on GitHub? Use the URL form: /plugin marketplace add https://github.com/mdsohaib/screenshot-time-machine.git
Claude Code opens screenshots with its Read tool, and stm --json hands it absolute paths. The loop is: edit, stm --json, Read the fold images, fix, repeat. Ask it to "check the UI" and it will reach for the skill on its own.
Put the memory line in AGENTS.md. Codex opens images with view_image, so if it hesitates, say "view the fold image of every page you touched".
Put the memory line in a rule under .cursor/rules/. The agent runs stm --json in its terminal and opens the PNG paths it returns. Cursor's built-in browser shows one page at a time; stm hands it all of them at once.
stm skill prints the skill text. Paste it into whatever instruction file your agent reads. This works without installing anything:
npx screenshot-time-machine@latest skill
What the agent gets back
{
"code": 0,
"url": "http://localhost:3000",
"dir": "/Users/you/app/screenshots/localhost-3000_2026-08-15_14-32",
"previous": "/Users/you/app/screenshots/localhost-3000_2026-08-12_09-10",
"ok": 7, "failed": 0, "saved": 7, "total": 7,
"changed": [
{ "path": "/pricing",
"file": "/Users/you/app/screenshots/localhost-3000_2026-08-15_14-32/pricing_2026-08-15_14-32.png",
"fold": "/Users/you/app/screenshots/localhost-3000_2026-08-15_14-32/fold/pricing_2026-08-15_14-32.png",
"height": 2140 }
],
"new": [], "removed": [], "truncated": 0,
"failures": [], "warnings": [], "interrupted": false,
"browserChanged": null,
"manifest": "/Users/you/app/screenshots/localhost-3000_2026-08-15_14-32/manifest_2026-08-15_14-32.json",
"hint": "View the fold image of each changed/new page; full-page PNGs taller than ~2000px are downscaled by the model.",
"gitignoreAdded": false
}
One JSON document on stdout, at most two lines on stderr, printed even when some pages fail. The one-time browser download is the exception: its installer also writes to stderr. code mirrors the exit code.
A run that cannot start prints a much smaller object instead: {"code": 1, "error": "no_server" | "no_browser", "message": "...", "fix": "..."}. Bad arguments print {"error": "bad_args", "message": "...", "fix": "..."}, where fix is Run stm --help for an unknown flag and a worked example for a malformed --url. Check error before you read changed. truncated counts entries left out of the list (the cap is 25; the manifest has them all). The output carries paths and numbers only, never text taken from your pages.
About tokens. Every page is captured every run, but the agent only opens the ones you point it at, so a quiet run costs almost nothing to read. The summary is roughly 200 tokens plus about 90 per listed page. Viewing one page costs a single 1440x900 image. Very tall full-page screenshots get downscaled by vision models until the text is unreadable, which is exactly why every page also gets a fold image, and why the skill tells the agent to open that one first.
A visual history on disk
screenshots/
├── localhost-3000_2026-08-01_09-14/ three weeks ago
├── localhost-3000_2026-08-08_18-02/ last Friday
└── localhost-3000_2026-08-15_14-32/ just now
├── manifest_2026-08-15_14-32.json what was captured, from where, with which browser
├── index_2026-08-15_14-32.png full page, 1440 px wide, as tall as the page
├── pricing_2026-08-15_14-32.png
├── blog--hello-world_2026-08-15_14-32.png "/" becomes "--" in file names
└── fold/
├── index_2026-08-15_14-32.png the top 1440x900 of each page, for quick reading
└── pricing_2026-08-15_14-32.png
Every run is a new folder named after the app and the minute it ran. Every file inside carries the same timestamp, so a screenshot dragged out of its folder still says what it is and when it happened. Two runs in the same minute get -2, -3 on the timestamp. Pages taller than 16,384 px are cut at that height, Chromium's limit, and marked truncated.
Sort the folders by date and you are looking at your product's timeline. Pick one page and step through its file in each folder and you are watching it get better. It is plain PNGs all the way down, so this history outlives the tool that made it: no database, no proprietary format, nothing to export. Uninstalling leaves nothing behind but your screenshots/ folder and, if you had a .gitignore, one line in it.
Commands
| Command | What it does |
|---|---|
stm |
Snapshot every page of the app you have running on localhost |
stm /pricing /about |
Snapshot only these pages, in seconds. This is the edit-check loop |
stm --url https://your-site.com |
Snapshot any site by URL, local or live. A path starts the crawl there |
stm --mobile |
Also capture a 390x844 phone view (@mobile files) |
stm --max 100 |
Stop discovering after this many pages (default 100) |
stm --out screenshots |
Where snapshots go (default screenshots) |
stm --json |
Machine-readable summary on stdout, for agents |
stm list |
Past snapshots: folder, page count, changed, failed |
stm open |
Open the latest snapshot folder |
stm skill |
Print the agent skill |
Exit codes: 0 everything captured, 1 nothing captured (no app, no browser, bad arguments), 2 finished with some pages failed or the run interrupted. A failing page never stops the run.
A few things worth trying
stm --mobile # desktop and phone views of every page, same run
stm /checkout --mobile # the one flow you just touched, both sizes, in seconds
stm --url https://staging.example.com # any site you are allowed to capture, not just localhost
stm list # every snapshot you have ever taken, oldest to newest
stm open # jump straight into the latest folder
How it works
- Detect. Probe the usual localhost ports and pick the app that answers, or use
--url. - Discover. Read
sitemap.xmlif there is one, then follow<a href>links from page to page, staying on the same origin. - Capture. Load each page, scroll it so lazy content arrives, hold everything still, then save a full-page PNG plus a
foldimage of the top screen. - File. Write everything into a folder named after the app and the minute, with the manifest updated after every single page.
- Report. Print a one-line summary, including which pages came out different from the previous run.
Why the screenshots are steady
A screenshot tool is only as good as its consistency: capture the same page twice and you should get the same picture. That takes real work, and it is where most of this code lives.
- Pages are scrolled to the bottom before capture so lazy images and lazy sections load, then scrolled back to the top. Smooth-scroll sites are handled.
- Web fonts are awaited, so nothing is captured mid-swap from the fallback face.
- Animations are frozen at their end state, so fade-ins are fully visible and spinners hold still.
- The device pixel ratio is pinned to 1 and the viewport to 1440x900, so a screenshot is the same on your laptop, your desktop and CI.
- Dev toolbars (Astro, Nuxt, the Next.js badge) are hidden. Dev error overlays never are: the page is captured as it is and flagged with a warning, because a broken page is exactly what you want to see.
- Long-lived connections (HMR sockets, event streams) never stall a capture. Each page gets a 30 second budget and one retry, then it is recorded and the run moves on.
- The manifest is written after every page, so an interrupted run still leaves a folder
stm listcan read. - Links to
/logout,/deleteand friends are never followed. Neither are files,/api/, other origins, or your ownscreenshots/folder.
The payoff: two runs on the same machine produce byte-identical PNGs of a page whose content did not change.
Every page is captured on every run. What gets compared is not the images, it is their fingerprints:
- After saving a page's PNG,
stmcomputes a sha256 hash of that file's bytes. - The hash goes into that run's manifest, next to the page path.
- On the next run,
stmlooks up the same page in the previous run's manifest and compares the two hashes. - A different hash means the screenshot came out different. The same hash means it did not. No earlier entry means the page is new.
Old screenshots are never reopened, only the small manifest is read, so comparing a hundred pages is instant. You get page names and file paths, which is exactly what you need to open the right screenshots first, or to hand your agent the right ones. Region-level visual diffing is stm diff on the roadmap.
A page that shows a clock, a relative time like "3 minutes ago", a rotating hero image or a canvas animation genuinely does look different every run, and will be listed every run.
How it compares
| stm | Claude Code Desktop auto-verify | Playwright MCP, agent-browser, DevTools MCP | Percy, Chromatic | |
|---|---|---|---|---|
| Every page in one command | yes | one page at a time | one page per tool call | yes, in CI |
| Browsable history on disk | yes | no | no | in their cloud |
| Lists which pages came out different | yes | no | no | yes, in the cloud |
| Works in the terminal, Cursor, Codex | yes | Desktop only | yes | CI only |
| Runs entirely on your machine | yes | yes | yes | no |
| Free | yes | yes | yes | paid above a free tier |
These tools are complementary, not rivals. Use a browser tool when your agent needs to click around and interact. Use stm when you or your agent needs to see everything, all at once, and keep it.
Privacy
Runs 100% locally. stm talks only to your app's URL and, once, to Playwright's browser CDN if you have no Chrome or Edge. Your screenshots never leave your machine. No accounts, no API keys, no telemetry. The pages you capture load their own resources exactly as they would in your browser. Full threat model in SECURITY.md.
FAQ
Roadmap
Small on purpose. Vote by opening an issue.
stm diff: show exactly which regions of a page changed, and hand agents crops instead of whole pagesstm timelapse: turn a page's history into a GIF or MP4, with git commit captionsstm backfill: walk your git history and build the visual history you never captured- Signed-in captures (
--storage-state) - A local gallery with a time slider
Not planned: cloud sync, accounts, telemetry, a watch mode, a config file.
Contributing
Bug reports with a real run attached are the most useful thing right now. Start with CONTRIBUTING.md, then AGENTS.md for the module map and the rules. Every decision and its reason lives in DECISIONS.md.
About
Built by Mohammed Sohaib Uddin, because software that changes every day deserves a record of how it looked.
If stm shows you something you are glad you saw, a star costs nothing and helps other people find it.
MIT © Mohammed Sohaib Uddin
No comments yet
Be the first to share your take.