Take automated screenshots, PDFs, HTML, HAR files and screen recordings with Chrome, from the command line or from a YAML file.
shots needs Chrome or Chromium and nothing else: no Node, no Python, no
Playwright, no WebDriver server. ffmpeg is needed only by shots video.
go install github.com/corneliusio/shots/cmd/shots@latestRelease archives for macOS, Linux and Windows, with a SHA256SUMS manifest, are
attached to each GitHub release.
shots uses a Chrome it finds on your machine. To get a pinned, known-good
build instead:
shots install # downloads Chrome Headless Shell into the user cache
shots doctor # reports the browser, cache and ffmpeg shots will useshots install verifies a SHA-256 checksum committed to this repository before
extracting, and never calls a system package manager. shots doctor exits 1 if
it finds a problem:
shots 1.0.0
platform darwin/arm64
pinned Chrome Headless Shell 152.0.7977.64 for mac-arm64
browser /Users/you/Library/Caches/shots/chrome-headless-shell/152.0.7977.64/…/chrome-headless-shell
cdp ok, HeadlessChrome/152.0.7977.64
cache /Users/you/Library/Caches/shots (writable)
ffmpeg /opt/homebrew/bin/ffmpeg
Chrome is resolved in this order: --browser-path, $SHOTS_BROWSER_PATH, the
Chrome Headless Shell build from shots install, a headless shell, Chrome or
Chromium on PATH, then the usual platform locations. --cdp-url connects to
a browser you started yourself instead of launching one.
Chrome Headless Shell is the default managed browser because it starts faster
and shots normally does not need Chrome's visible application UI. shots auth,
--interactive and --devtools skip the installed shell and resolve a system
Chrome or Chromium instead. Use --browser-path when you need a specific full
Chrome build. The shell may not inherit the same system color preference as
full Chrome, so pass --color-scheme light or --color-scheme dark when
comparing their output.
shots take example.com # example-com.jpg
shots take example.com -o home.png -w 1280 -h 720
shots take https://example.com -s '#main' -p 20 -o main.png
shots javascript example.com 'document.title'
shots pdf example.com -o page.pdfEvery invocation names a command. Use shots take example.com for a screenshot;
shots example.com is a usage error. shots on its own prints the help.
Every command that loads a page shares one lifecycle: resolve Chrome, open an isolated
browser context, restore --auth state, configure the viewport, navigate, apply
--skip/--fail, wait, run JavaScript, write the artifact, tear everything
down. So the shared flags (--auth, --timeout, --wait, --wait-for,
--javascript, --js-file, --interactive, --skip, --fail, --silent,
--browser-path, --cdp-url, --insecure, …) mean the same thing everywhere.
Run shots <command> --help for the full list.
Use --color-scheme light, dark, or no-preference when the screenshot
must not depend on the machine's current appearance setting. Leaving it out
keeps Chrome's default.
--insecure accepts TLS certificates Chrome would reject, which is what a local
development server with a self-signed certificate needs. It is applied to the
page over CDP, so it also works with --cdp-url.
A local HTML file loads from a file:// URL, and Chrome does not let such a
page read other local files: a canvas drawn from a local image is tainted, and
fetch of a sibling file fails. Pass
--browser-arg=--allow-file-access-from-files when a page needs that.
Screenshots. The default width is 1280; omitting --height captures the full
page. Output defaults to JPEG quality 80. Use --format png or a .png output
filename for lossless output; --omit-background also selects PNG because JPEG
cannot preserve transparency. Repeated selectors capture their combined
bounding rectangle, and --padding grows it. Chrome optimizes screenshot
encoding for speed, which makes PNG files larger than its size-optimized
default without changing their pixels.
Element captures scroll the selected region toward the viewport center before Chrome encodes its document-space clip. This avoids Chrome substituting fixed or sticky content from elsewhere on the page into an offscreen clip. During capture, shots hides only intersecting fixed or sticky elements outside the selected DOM trees, then restores their inline styles. Chrome encodes the selected region directly, including regions larger than the viewport.
shots take https://example.com -h 720 --retina -o hero.jpg
shots take https://example.com -s '#nav' -s '#footer' -p 16 -o chrome.jpg
shots take https://example.com --format png -o lossless.png
shots take https://example.com -o - > shot.jpg
shots take https://example.com --stable --report - -o shot.pngFor a fast edit-and-check loop, --repeat takes the first screenshot
immediately, then takes another for every line read from standard input. It
keeps one Chrome process, browser context and page alive, navigates the same URL
with the same options, and atomically replaces the output file each time. EOF
ends the loop; Ctrl-C uses the normal cleanup path.
shots take http://localhost:5173 --repeat --stable --report - -o shot.png
# press Enter after each edit; stdout is one JSON report per captureRepeat mode requires an explicit file output and cannot be combined with
-o -, --no-clobber, --js-file -, interactive/devtools mode, or HAR
recording. It deliberately does not watch files. A failed capture writes its
report and an error line to stderr, then the loop goes on, so a script that
throws once does not end the session; at EOF the exit status is 1 if any capture
failed.
--cdp-url attaches to a Chrome process that is already listening for DevTools
connections. shots leaves that process running but creates and disposes an
isolated browser context and page for each command. This removes Chrome process
startup across separate CLI calls; it does not reuse a tab or session state the
way --repeat does. shots browser is the easiest way to start such a process.
Prefer --repeat for one page, multi for a known group of pages, and
shots browser with --cdp-url when separate commands need to share one
browser.
--stable opts into a short pre-capture stabilization pass: it emulates
prefers-reduced-motion, disables CSS animations and transitions, hides text
carets, waits for discovered fonts and images, then waits for two paint frames.
It does not guess at network-idle or mock time.
--report FILE writes one compact JSON object on success or capture failure;
--report - writes it to stdout. Reports contain schema, ok,
requested_url, final_url, status, output, duration_ms, console,
page_errors, failed_requests, omitted, and error. Each diagnostic list
is capped at 100 entries, with excess counts in omitted. console includes
page console calls and browser-generated messages such as failed resource
loads. In repeat mode a report file contains the latest capture, while
standard output is JSON Lines.
Image bytes, response bodies, HTML and successful request lists stay out of the
report.
Explicit output paths must have an existing parent directory. If the parent is missing, shots names that directory directly instead of exposing its random temporary-file path.
Runs a YAML list of shots, shell steps and servers, in source order, against one Chrome process and one browser context — so an entry that signs in is still signed in for the next.
- server: ./scripts/serve
- url: http://localhost:8000/
output: home.png
height: 720
- sh: node scripts/seed.js
- url: http://localhost:8000/pricing
output: pricing.jpg
quality: 80
selectors:
- .plans
wait_for: document.querySelectorAll('.plan').length === 3shots multi shots.yml
shots multi shots.yml -o home.png # take only this entry's shot
shots multi shots.yml --stable --report reports.jsonl
shots multi - < shots.yml--stable applies the same finite stabilization pass to every selected entry.
--report writes one JSON object per selected entry as JSON Lines, including
failed shots when the batch continues. The batch still uses one Chrome process
and browser context; each entry gets a fresh page.
Entry keys: url, output, width, height, format, quality, omit_background,
wait, wait_for, javascript, js_file, padding, selector/selectors,
selector_all/selectors_all, js_selector/js_selectors,
js_selector_all/js_selectors_all, plus the step keys sh and server.
Anything else is rejected with its field path
(entries[3].retina: unknown field) and exit status 2. Relative paths resolve
against the configuration file's directory; a configuration on standard input
resolves against the current directory.
A failed sh: step stops the run, because everything after it was written
expecting it to have run. A failed shot is reported and the run carries on,
unless --fail-on-error. Servers are stopped — process group and all — on
success, failure, timeout and interrupt, unless --leave-server.
Runs JavaScript on the page and prints the result as JSON on stdout. Promises are awaited, and a script that evaluates to a function is called.
shots javascript example.com 'document.title'
shots javascript example.com --js-file links.js
shots javascript example.com -r 'document.title' # unquoted string
echo 'location.href' | shots javascript example.comThe rendered document, or the outer HTML of the first element matching
--selector.
shots html example.com -o page.html
shots html example.com -s '#main'Page.printToPDF, streamed to disk rather than round-tripped through base64.
Paper formats are Letter, Legal, Tabloid, Ledger and A0 to A6;
--width/--height take in, cm, mm or px, and a bare number is pixels.
shots pdf example.com --format A4 --print-background -o page.pdf
shots pdf example.com --media-screen --landscape -o wide.pdfChrome's accessibility tree, filtered to a stable JSON format: role, name,
value, description, states, properties and ordered children. Ignored nodes and
InlineTextBox nodes are dropped, and nothing exposes Chrome's transient node
identifiers.
shots accessibility example.com -o a11y.jsonHAR 1.2 built by correlating CDP network events, with response bodies fetched as each request finishes so navigation cannot evict them.
shots har example.com -o trace.har
shots har example.com -z -o trace.har.zip
shots har example.com -x # also extract the captured resourcestake and multi accept --har, --har-zip and --har-file to record
alongside their own output; on multi that is one archive for the whole run.
Records a scripted browser demonstration to WebM, and optionally MP4. Needs
ffmpeg, which is checked before anything is launched or run.
url: http://localhost:8000/
output: demo.webm
viewport: {width: 1280, height: 720}
cursor: {visible: true, clicks: true}
scenes:
- name: Search
do:
- pause: 1
- click: '#search'
- type: {selector: '#search', text: shots, delay_ms: 90}
- press: Enter
- wait_for: '.results'
- scroll: {y: 600, duration: 1.2}
- screenshot: results.pngshots video demo.yml --mp4
shots video demo.yml -o /tmp/demo.webm --keep-framesTop-level keys: output, url, viewport, cursor, wait, wait_for,
wait_for_url, javascript, sh, server, scenes. A scene takes
name, open, wait_for, wait_for_url, sh and do. Actions are
open, click, fill, type, press, scroll, pause, wait_for,
wait_for_url, javascript/js, screenshot and sh; each accepts
a scalar shorthand (click: '#go') or a mapping of its own keys
(click: {selector: '#go', button: right}).
If encoding fails, the command exits 1 and keeps the captured frames, naming the
directory. --keep-frames keeps them after a successful encode too.
Opens a headed browser, waits for you to sign in and press Enter, then writes
cookies and per-origin localStorage as a Playwright storage_state file with
mode 0600.
shots auth https://example.com/login auth.json
shots take https://example.com/account -a auth.json -o account.pngThe state file uses Playwright's storage_state JSON format.
Launches Chrome once, prints its DevTools URL on standard output, and stays in
the foreground until it is interrupted or Chrome exits. Every other command then
attaches to it with --cdp-url, skipping Chrome startup while keeping its own
isolated browser context and page. This is the launch half of --cdp-url, so it
takes the launch options --cdp-url rejects: --browser-path and
--browser-arg. --port fixes the loopback port; the default is a free one.
shots browser > browser.url &
shots javascript https://example.com 'document.title' --cdp-url "$(cat browser.url)"
shots take https://example.com --cdp-url "$(cat browser.url)" -o page.jpg
kill %1Chrome binds the port to loopback. Do not expose it to another machine: a CDP client can fully control that browser.
Downloads the pinned Chrome for Testing Headless Shell build into
<user-cache-dir>/shots/chrome-headless-shell/<version>/<platform>/, verifying
the committed SHA-256 checksum before extracting. --force re-downloads.
The installed shell handles every headless command. Visible auth,
--interactive and --devtools sessions still require full Chrome or Chromium
on the machine, or an explicit --browser-path.
shots installReports the version, platform, resolved browser, CDP connectivity, cache
permissions and ffmpeg. Exits 1 if anything is a problem — including
Linux shared libraries ldd reports missing for the browser.
shots doctorstdout carries only what you asked for: image bytes for -o -, a JSON result
or capture report, HTML, a HAR. Progress and errors go to stderr, and --silent suppresses
progress but never a fatal error. Files are written to a temporary file in the
destination directory and renamed into place, so a failed command leaves no
misleading artifact and no half-written file. Parent directories are never
created for you.
| Status | Meaning |
|---|---|
| 0 | Success, including a page that --skip skipped |
| 1 | Browser, network, filesystem, process or runtime failure |
| 2 | Invalid arguments or configuration |
sh:, --javascript, --js-file and gh: remote scripts are
trusted user code and are not sandboxed. They run with your privileges. Only
run configurations and scripts you would run yourself.
shots never logs cookies, storage values, passwords or authorization headers.
Authentication state files are written with mode 0600. HAR extraction rejects
absolute paths and directory traversal. A browser is downloaded only when you
run shots install, only from the pinned manifest, and only after its checksum
matches.
go test ./...
go test -race ./...
go vet ./...Integration tests drive a deterministic local site embedded in
internal/testsite; no test touches a public website. go run ./tools/pin-chrome
re-pins the Chrome for Testing Headless Shell build and rewrites its checksum
manifest.
MIT. See LICENSE.