Browse the docs

CLI reference

get-handoff captures a walkthrough: it drives a real browser through a plan you write in Markdown, shoots a frame per step, and uploads them as a draft your agent then publishes.

Install and requirements

Published on npm as get-handoff; run it with npx and skip the install. The binary answers to both get-handoff and handoff.

npx get-handoff walk .handoff/walkthroughs/checkout.md \ --api https://makhtriiptwkicmksbdr.supabase.co/functions/v1 \ --key fbt_your_key

It needs Node 20 or newer — the floor playwright-core sets — and a locally installed Google Chrome — the package ships playwright-core, which drives a browser but never downloads one. Chromium, Edge and Firefox are not substitutes.

By default it launches its own persistent profile at ~/.handoff/browser, so a site you log into once stays logged in for later captures. If the app you're recording keeps its session in memory, or you just want to record what's already on screen, attach to a Chrome you started yourself:

open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=/tmp/handoff-rec npx get-handoff walk plan.md --attach http://localhost:9222 --api … --key …

Attaching takes the first tab of the first context and resizes that window to 1280×800 — it does not pick whichever tab you happen to be looking at, and it does move your window.

The key is never stored: pass --key or HANDOFF_KEY on every call. There is no login step and no config file.

Commands

walk

get-handoff walk <plan.md> [flags]

Runs the plan in a browser and captures one frame per step, then prints the walkthrough's wlk_… id. The first run of a plan creates a draft: nothing is public until you publish it.

Re-running the same plan file updates that same walkthrough in place rather than making a second one. That keeps the link stable — and it means that if the walkthrough is already published, the fresh frames go live immediately, without a publish step. The CLI says “draft ready” either way, so check list if you're unsure which one you're re-shooting.

A step that fails doesn't abort the capture: it's recorded as failed and listed at the end, and the draft is still created. Pass --fail-fast if you'd rather fix the plan and re-shoot than upload a broken capture. With --local there is no draft to skip, so --fail-fast does nothing there and the file is written either way — the run says so.

publish

get-handoff publish <wlk_id> [--summary file.json]

Turns a draft into a public link, optionally attaching the “what changed” summary. Your agent can do the same thing over MCP with publish_walkthrough, which is usually where the summary comes from — it's written after looking at the captured frames.

list

get-handoff list

Every walkthrough belonging to your key, newest first, with its status, plan key and title; published ones also show their link. No filters, no paging.

prune

get-handoff prune

Deletes all drafts of your key in one go. Published walkthroughs are left alone. There is no confirmation prompt and no dry run.

revoke

get-handoff revoke <slug|wlk_id>

Closes the public page for that walkthrough. The record itself stays — this switches the link off, it does not delete the capture.

Flags and environment

FlagDefaultWhat it does
--api <url>local Supabasebase URL of the Handoff functions; required against the cloud
--key <key>noneyour agent key
--attach <url>offdrive an already-running Chrome over CDP instead of launching one
--headlessoffhide the window; applies to the launched browser only, ignored when attaching
--settle <ms>400extra pause after each step before the frame is taken
--no-clipoffskip the screencast; the viewer doesn't play it, so this is a free speed-up. --local implies it — nothing there consumes a clip
--fail-fastoffif any step failed, don't create the draft; no effect with --local, which has no draft to skip and writes the file anyway
--out <file>offwrite the request body to a file instead of creating a draft; frames are still uploaded first when a key is present
--local <file>offwrite a self-contained HTML walkthrough instead of creating a draft; needs no key and uploads nothing, so it can't be combined with --out and implies --no-clip. Every frame is inlined, so the file grows with the step count — the run prints its size and warns past 25 MB
--summary <file>nonepublish only; JSON array of { stepIdx, text }

HANDOFF_API, HANDOFF_KEY and HANDOFF_SETTLE set the same three values from the environment; the flags win. Flags take their value as the next argument — --api=URL is not understood.

The plan file

A plan is Markdown with a small front matter block and one numbered line per step. The point of writing it down is that a plan is cheap to fix and a capture is not — you edit text until the flow is right, then shoot.

--- title: New checkout origin: http://localhost:3000 --- 1. open /cart — The cart 2. click button.promo-apply — Apply a promo code 3. type input.promo "SALE20" — Enter the code 4. check .cart-total — Total recalculates 5. scroll .terms — Terms below the fold 6. wait .deal-card — hold for the card to render

origin is required and title is optional. Front matter is only recognised when the very first line of the file is ---. Lines that aren't numbered steps are ignored, so you can keep notes between them.

Steps

Every step reads <action> <target> — <caption>. The caption is what the viewer reads next to the frame, so write it for the person you're sending this to.

ActionTargetWhat happens
openpath or URLnavigates; a relative path is appended to origin as-is, so it must start with /
clickselectorclicks the first match
typeselector + textfills the field; the text goes last, in double quotes
checkselectorwaits for the element to be visible
scrollselectorscrolls the element into view
waitselector or mswaits; no frame is captured and no caption is needed

Targets are Playwright selectors, so button:has-text("Place order") and text=… work — useful on markup where the class names are utility soup. An ambiguous selector doesn't fail the step: the first match wins.

Where plans break

The caption separator is the first em dash in the line; only if there is no em dash at all does the parser fall back to the first space-hyphen-space. Quoting the target does not protect it — a target containing either sequence gets cut in half, wherever it sits. That's the failure worth memorising, because the step still runs, just against the wrong selector.

Targets with spaces need no quotes at all. Single quotes around a target are still accepted for compatibility, but an unbalanced one silently becomes part of the selector.

The rest of the parser fails loudly and tells you the line number: an unknown action, a missing caption, a type without its quoted text, an empty target, a plan with no origin or no steps.

Re-shooting

A walkthrough's identity is the plan's filename, minus the extension. Re-running the same file updates the same walkthrough: the frames are replaced, the link and any published summary survive. Rename the file and you get a new walkthrough with a new link.

Sharp edges

The default API is localhost

Without --api the CLI talks to a local Supabase on port 54321. Against the cloud, always pass it. If you happen to be running that local stack, the symptom is an “invalid key” error with a perfectly valid key; if you aren't, it's a bare fetch failed.

No key means the capture is lost — unless you pass --local

The browser runs, then the upload has nowhere to go: a plain run exits with code 2 and the frames are gone. --local <file> writes them to one self-contained HTML page instead. --out is not a rescue — it writes the request body, and without a key that body carries no images at all.

Exit code 1 doesn't mean “nothing happened”

A capture with a failed step exits non-zero and creates the draft. Only --fail-fast skips it — and with --local nothing is skipped at all: the file is written, and the failed steps are in it.

Don't close the window it drives

Closing the window mid-run — yours or the one it launched — ends the run there. The steps captured so far still become a draft.

Mostly-open plans look dead

The viewer animates a cursor toward the target of a step, which needs both a target and a previous frame to move from. Navigations have no target, and the first step has nothing before it, so both cut straight to the result.