Gotcha

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 list shows open sessions, gotcha close --all ends them.
  • open takes --width and --height (1440 × 900 by default), --scale (2, for sharp captures on dense screens), and --headed for a visible window.
  • --json on 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

OptionFrames
--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-pagethe whole page, scrolling included
nothingthe 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.

OptionDraws
--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, --ellipsea 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
A “New payment” dialog with three red numbered badges: 1 on the amount field, 2 on the customer field, 3 on the Create payment button.
Number a walkthrough in one command. gotcha shot --selector ".panel" --padding 26 \ --step ".field-amount .fld" --step ".field-customer .fld" --step ".btn-create"
A payments table. One row is framed in red, a label inside it reads “Same card as the two refunds last week”, and all five customer email addresses are blurred out while the names stay readable.
Point at one thing, and redact the rest. 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.

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 chromium puts one in Playwright's cache, and --extension looks there first; --chrome or GOTCHA_CHROME override it.
  • An extension that relies on activeTab needs 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 →