Simple, stable full-page screenshot CLI tool.
Not a smart analyzer — a dumb, reliable screenshotter. One URL → one full-page screenshot.
npm install -g mshot
npx playwright install chromiummshot --url <url> --out <file> [options]
mshot batch --url <url> --out-dir <dir> [options]Capture a single page:
mshot --url https://example.com --out screenshot.jpgCapture multiple pages with link discovery:
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture --discover --max-pages 12
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture --viewports desktop,mobile --max-height 900For deterministic capture of specific routes (e.g. important app screens),
use --urls-file with a plain text file — one URL or path per line:
mshot batch --url http://localhost:4321 --out-dir tmp/visual-capture --urls-file .mshot/visual-routes.txt
mshot batch --url http://localhost:4321 --out-dir tmp/visual-capture --urls-file .mshot/visual-routes.txt --discover --max-pages 12File format (plain text, one entry per line):
/
# important app screens
/projects
/project/demo/errors
http://localhost:4321/settings
Rules:
- Empty lines and lines starting with
#are ignored - Leading/trailing whitespace is trimmed
- Relative paths (starting with
/) are resolved against--url - Absolute URLs are allowed only if same-origin as
--url - Hash fragments are stripped
- Exact duplicates are removed
- External origins are skipped (recorded in
manifest.jsonskipped[])
With --urls-file:
- Explicit input URLs are captured first, before any discovered URLs
- Explicit input URLs are not removed by route pattern deduplication
- If
--discoveris also used, discovered URLs fill remaining--max-pagesslots - If
--urls-fileis not specified, behavior is unchanged (base URL + optional discover)
| Flag | Default | Description |
|---|---|---|
--url <url> |
(required) | Target URL (http/https) |
--out <file> |
(single) | Output path (.jpg, .jpeg, .png, .webp) |
--out-dir <dir> |
(batch) | Output directory for batch mode |
--width <px> |
1440 |
Viewport width |
--max-height <px> |
none | Crop to this height if page is taller |
--quality <1-100> |
82 |
JPEG/WebP quality |
--timeout <ms> |
30000 |
Page load timeout |
--wait <ms> |
500 |
Extra wait after load |
--no-pre-scroll |
Skip pre-scroll stabilization | |
--no-settle |
Skip font/image settle and animation normalization | |
--settle-timeout <ms> |
3000 |
Settle timeout ceiling (not a fixed sleep) |
--viewports <list> |
desktop |
Comma-separated: desktop, mobile |
--discover |
false |
Discover rendered links from base page |
--max-pages <n> |
12 |
Max pages to capture |
--concurrency <n> |
2 |
Max concurrent captures |
--networkidle-timeout <ms> |
2000 |
networkidle wait timeout per page (batch) |
--max-per-pattern <n> |
1 |
Max URLs per route pattern (dedupe) |
--no-route-dedupe |
Disable route pattern deduplication | |
--depth <n> |
1 |
Link discovery depth: 1 = base only, 2 = one level deeper |
--urls-file <file> |
Explicit URL/path list (plain text, one per line) | |
--segments |
false |
Batch only: Create overview + overlapping segments |
--segment-height <px> |
2200 |
Segment height in pixels (requires --segments) |
--segment-overlap <px> |
300 |
Overlap between adjacent segments (requires --segments) |
Single mode:
Success: stdout = path | stderr = empty or MSHOT_LIMITED | exit 0
Failure: stdout = (empty) | stderr = MSHOT_ERROR: ... | exit 1
Batch mode:
Success (≥1 page): stdout = manifest.json path | exit 0
Failure (0 pages): stdout = (empty) | stderr = MSHOT_ERROR: ... | exit 1
Warning: MSHOT_WARN: skipped pages
mshot --url https://example.com --out example.jpg
mshot --url https://example.com --out example.webp --quality 50
mshot --url https://example.com --out example.jpg --max-height 20000
mshot --url https://example.com --out example.jpg --no-pre-scroll
mshot --url https://example.com --out example.jpg --no-settle
mshot --url https://example.com --out example.jpg --settle-timeout 1000
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture --discover --max-pages 12
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture --viewports desktop,mobile --max-height 900
mshot batch --url http://localhost:4321 --out-dir tmp/visual-capture --urls-file .mshot/visual-routes.txt
mshot batch --url http://localhost:4321 --out-dir tmp/visual-capture --urls-file .mshot/visual-routes.txt --no-settleBatch mode writes a manifest.json to --out-dir:
pages[]— captured pages with screenshot paths and timingsskipped[]— failed pages, deduplicated duplicates, or overflow from--max-pagesviewports[]— viewport definitions- First page is always the base URL (source:
base) - Discovered links have source:
rendered-link - Explicit input URLs (from
--urls-file) have source:input - Same-origin only, no assets/mailto/tel/hash links
Each page record includes timing breakdown:
gotoMs— navigation timenetworkidleMs—networkidlewait timepreScrollMs— pre-scroll time (if enabled)fontWaitMs— font settle time (if settle enabled)imageWaitMs— image settle time (if settle enabled)screenshotMs— screenshot capture timeoverviewMs— overview thumbnail creation time (if--segments)segmentationMs— segment crop time (if--segments)outputMs— file output time (if--segments)totalMs— total capture time (includes post-processing when segments enabled)
When --segments is used, each page record includes additional additive fields:
segments[viewport][]— array of{ file, x, y, width, height }for each segmentoverview[viewport]—{ file, sourceWidth, sourceHeight, width, height }for the overview thumbnailscreenshots[viewport]— remains a string path, now pointing to the overview file
By default, --discover captures at most 1 URL per route pattern.
This prevents capturing dozens of entity-instance pages that look the same visually.
Pattern heuristic: numeric/UUID/long/encoded segments → :id
/project/foo→/project/:id/project/bar→/project/:id/project/foo/errors→/project/:id/errors
Duplicates are recorded in manifest.json under skipped[] with reason: "duplicate-pattern".
--depth 1(default): base page + links from base page--depth 2: after dedupe, open selected representative pages and discover their links too
For long pages, --segments creates an overview thumbnail and a sequence of overlapping segments from a single browser capture.
All post-processing uses Sharp — no additional browser opens or screenshots.
mshot batch --url http://localhost:3079 --out-dir tmp/visual-capture --segmentsHow it works:
- Navigate → settle → screenshot (one capture per viewport)
- Apply
--max-heightif specified (crops the full-page buffer) - Create overview thumbnail via Sharp (max height 3000px, preserves aspect ratio)
- Create overlapping segments via Sharp crops
- Write all files atomically (overview + all segments)
- Write manifest last (commit point)
Filenames:
For route /project/demo, viewport desktop:
project-demo-desktop-overview.jpg— full-page thumbnailproject-demo-desktop-segment-001.jpg— first segmentproject-demo-desktop-segment-002.jpg— second segment (overlaps 001)- ...
Manifest structure with --segments:
{
"screenshots": {
"desktop": "project-demo-desktop-overview.jpg"
},
"overview": {
"desktop": {
"file": "project-demo-desktop-overview.jpg",
"sourceWidth": 1440,
"sourceHeight": 17668,
"width": 244,
"height": 3000
}
},
"segments": {
"desktop": [
{
"file": "project-demo-desktop-segment-001.jpg",
"x": 0,
"y": 0,
"width": 1440,
"height": 2200
},
{
"file": "project-demo-desktop-segment-002.jpg",
"x": 0,
"y": 1900,
"width": 1440,
"height": 2200
}
]
}
}Key points:
- Feature is batch-only and opt-in (
--segmentsrequired) screenshots[viewport]remains a string path → points to overview- Detailed crops are in
segments[viewport][] --max-heightis applied before segmentation- Single browser capture → Sharp overview/segments
- Atomic multi-file write (all or nothing)
- Segments overlap by
--segment-overlappixels
mshot batch \
--url http://localhost:3079 \
--out-dir tmp/visual-capture \
--discover \
--depth 2 \
--max-pages 8 \
--max-per-pattern 1 \
--viewports desktop \
--networkidle-timeout 2000 \
--max-height 20000- Fresh Chromium (headless)
- Navigate → wait
domcontentloaded→networkidle - Pre-scroll top→bottom→top (reveals lazy images, IntersectionObserver content)
- Settle — best-effort wait for
document.fonts.readyand<img>load (ceiling:--settle-timeout) - Optional
--wait <ms>extra pause - Full-page screenshot with
animations: 'disabled' - Atomic write (tmp → rename)
- Stable over clever — predictable behavior > smart behavior
- Agent-safe — clean stdout/stderr, atomic writes, no stale files
- No DOM parsing, no AI, no analysis — just screenshot and save
- One browser per batch — discovery reuses the batch browser
- Route pattern dedup — avoids capturing 50 identical entity pages
- Batch captures pages and writes manifest. It does not analyze screenshots or call models.
MIT
{ "manifestVersion": 1, "baseUrl": "http://localhost:3079", "createdAt": "2026-07-06T...", "viewports": { "desktop": { "width": 1440 }, "mobile": { "width": 390 } }, "pages": [ { "url": "http://localhost:3079", "source": "base", "screenshots": { "desktop": "home-desktop.jpg" }, "timings": { "gotoMs": 120, "networkidleMs": 80, "preScrollMs": 3500, "fontWaitMs": 15, "imageWaitMs": 50, "screenshotMs": 200, "totalMs": 4000 } } ], "skipped": [ { "url": "http://localhost:3079/project/abc123", "source": "rendered-link", "reason": "duplicate-pattern", "pattern": "/project/:id" } ] }