The command line
A mouse frames. An agent names.
An agent cannot see the screen, but it knows the selectors, because
it just read the HTML. Every framing and every annotation target of
gotcha is a CSS selector, and the same renderer as the
extension turns it into pixels.
Install
npm install -g @saastisfaction/gotcha
gotcha help
Node 20 or later, and Google Chrome on the machine — the system's
Chrome, because it carries the same fonts and engine as what the
readers of your documentation will see. If Chrome is somewhere
unusual, point at it with GOTCHA_CHROME=/path/to/chrome
or --chrome on gotcha open.
The package weighs 60 KB and depends on playwright-core
only — no browser download, no binary. It ships the Claude Code skill
under skills/gotcha/; see
Write docs with an agent to plug
it in.
One session, kept alive
gotcha open https://app.example.com/invoices --session doc gotcha text --session doc "h1" # check you are on the right screen gotcha shot --session doc --selector "#invoices" --out doc/01.png gotcha close --session doc
open launches a real Chrome, detached, and every
following command reconnects to it. The login, the scroll position,
the open menu survive between commands. Reopening the browser for
every screenshot is the one thing that would actually cost you time —
and the state of the page.
--session <name>on any command; default otherwise. Several journeys can run side by side.gotcha listshows open sessions,gotcha close --allends them.opentakes--widthand--height(1440 × 900 by default),--scale(2, for sharp captures on dense screens), and--headedfor a visible window.--jsonon any command returns one machine-readable line.
Get your bearings before shooting. A screenshot
taken blind on a loading screen is discovered too late, once it is
already in the documentation. gotcha text "h1" costs
almost nothing.
Framing
| Option | Frames |
|---|---|
--selector "#invoices" | the element, with --padding 16 around it (12 by default). It is scrolled into view first. |
--region "0,0,900,400" | a rectangle of the page, when nothing in the DOM fits |
--full-page | the whole page, scrolling included |
| nothing | the visible part |
A selector can only frame what has been rendered: for a card taller
than the window, open the session with a taller --height
rather than getting a silently truncated image.
Annotating
Every target is a CSS selector, or a point x,y, or a
rectangle x,y,width,height for the cases where nothing in
the DOM corresponds — a spot on a map, an area of a canvas.
| Option | Draws |
|---|---|
--arrow "<target>" | an arrow to the target, from the top left. --arrow "a -> b" joins two points when you want to control where it starts. |
--rect, --ellipse | a frame, a circle — outline without hiding |
--step <target> | a numbered badge, in the order you write the options — the order of the journey, not of the DOM |
--text "Label|<target>" | a label, placed in the nearest clear space around its target and never outside the frame. On a very dense screen, give a position: "Label|420,300". |
--blur <target> | a destructive mask, on every match |
--color #RRGGBB, --stroke <n> | the hue and the thickness |
gotcha shot --selector ".panel" --padding 26 \
--step ".field-amount .fld" --step ".field-customer .fld" --step ".btn-create"
gotcha shot --selector "#payments" --padding 18 --rect ".row-4" \
--text "Same card as the two refunds last week|.row-4 .pill" --blur ".customer-email"
Masking personal data
gotcha shot --session doc --blur ".customer-email" --blur ".phone" --out doc/03.png
--blur is destructive: the pixels are destroyed, and
nothing is recoverable from the delivered file. A selector matching
forty rows masks all forty — which is the case that actually leaks.
The block size follows the capture scale, so a mask is as opaque on a
2× image as on a 1× one.
Go through it before any screenshot meant to leave the organisation — public documentation, a support ticket, a release note. An address left in a PNG cannot be taken back once the file is sent. And look at the image, not only at the exit code.
Navigating
gotcha click ".btn-new" --session doc --wait ".modal" # waits for the next screen gotcha type "#amount" "1240" --session doc --enter gotcha press Escape --session doc gotcha wait ".spinner-done" --session doc gotcha scroll ".footer" --session doc gotcha text --session doc
Enough to walk a journey: click, type, press a key, wait for an
element or a delay, scroll to an element or a position, read the text
on screen. With --json, each command returns one line a
script can chain — handy for twenty screenshots in one go:
for page in invoices customers settings; do gotcha open "https://app.example.com/$page" --session doc gotcha shot --session doc --full-page --out "doc/$page.png" --json done gotcha close --session doc
Behind a login
Most interesting applications ask you to sign in, often with a second factor. Rather than teaching an agent to get through — fragile, and questionable — a visible window opens once, a human signs in, and the state obtained is reused indefinitely.
gotcha login https://app.example.com --session doc # a real window opens; sign in by hand gotcha save-login --session doc --out .gotcha-session.json gotcha open https://app.example.com --session doc --storage-state .gotcha-session.json
The file is in Playwright's storage-state format: cookies and local storage. Treat it as a credential. Screenshots behind a login are the one feature of the command line that needs a licence outright — see Licence & quotas.
When a selector fails
failed: no element matches ".invoice";
present in the page: .btn-invoice (×3), #invoices
The failure lists what the page really contains and what looks
close. That is worth more than a stack trace to a reader with no
screen — take the suggestion rather than reading the whole DOM again.
An unknown option is refused the same way, with its nearest
neighbour: a misspelled --blur that was silently ignored
would ship an unmasked capture.
Documenting a Chrome extension
gotcha open --extension ./dist loads an unpacked
extension into the session. Its pages —
chrome-extension://…/options.html and the like — are then
ordinary pages to gotcha shot, and so is the interface
it injects into a tab. Every screenshot of
the extension guide was taken this
way, by Gotcha's command line on Gotcha's extension.
gotcha open http://localhost:8080/ --session docs --extension ./dist --width 1280 --height 800 gotcha shot --session docs --step ".bar button[title=\"Capture an image\"]" --step ".bar button[title=\"Record a video\"]" --out docs/bar.png
- It needs a Chrome for Testing: since version 137,
Chrome stable ignores
--load-extension, so that no third-party software can install an extension behind the user's back.npx playwright install chromiumputs one in Playwright's cache, and--extensionlooks there first;--chromeorGOTCHA_CHROMEoverride it. - An extension that relies on
activeTabneeds a human gesture that no automation can give. For the shoot, load a copy whose manifest carries a host permission — never the one you will publish. - A closed shadow root is invisible to selectors, as it should be. If you own the extension, open it in the copy used for the shoot.
Filming, when one image is not enough
Gotcha photographs; it does not film from the command line. The
extension's video editor rests on tab capture, which requires a human
gesture. For a moving capture, drive Chromium with Playwright's
recordVideo and cut with the ffmpeg Playwright already
ships. The skill carries the seven rules that make such a video
watchable — a standard aspect ratio, the consequence inside the
frame, a drawn cursor, a hand's pace, two seconds after the last
gesture. Only film a sequence; for a state, an image is still better.
Reporting a bug, a missing selector, a wish
When an element cannot be targeted by any selector — a closed shadow root, a canvas, a frame — when something behaves wrongly, or when a feature is missing, say so from where you are and carry on:
gotcha feedback selector "The save bar lives in a closed shadow root; no selector reaches it" --url https://app.example.com/settings --selector ".save-bar" gotcha feedback bug "--blur on a selector inside an iframe masks nothing" --command "gotcha shot --blur '.email'" gotcha feedback feature "A left-side position for --text would help on dense tables"
Three kinds — bug, selector,
feature — then the message, and optional context:
--url, --selector, --command,
and --contact for an address to answer to. The reply is
one line with a reference (fb_…); with
--json, an id field. The licence key, if one
is set, goes along and identifies the sender — that is all it is used
for. Without one, one message a day; with one, as
many as needed. A refused message says when to try again; do not retry
before that. No customer data in the message: describe the field, do
not paste its content. The same form exists on
the feedback page, and it is a plain
POST to /api/feedback for anything else.
Look before you write: gotcha tickets --type selector
lists what has already been reported and where it stands — received,
confirmed, done, duplicate, declined, needs details — one line per
ticket, --status to narrow. It is the same list as
the support & improvements board,
which shows every report the moment it is received and the review's
one-line summary once it has been read. If yours is already there,
quote its id in your report instead of sending it again.
Quota and licence
gotcha shot is free up to 20 captures a day; screenshots
behind a login need a licence. gotcha license tells you
where you stand in one line, and with --json the
licensed field settles it without reading the sentence.
Whatever is built from the captures says “Made with Gotcha”,
with a link to this site and to npm —
the line to paste.
How a key reaches the machine, and what
an agent should do at the wall →