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_keyIt 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
| Flag | Default | What it does |
|---|---|---|
| --api <url> | local Supabase | base URL of the Handoff functions; required against the cloud |
| --key <key> | none | your agent key |
| --attach <url> | off | drive an already-running Chrome over CDP instead of launching one |
| --headless | off | hide the window; applies to the launched browser only, ignored when attaching |
| --settle <ms> | 400 | extra pause after each step before the frame is taken |
| --no-clip | off | skip the screencast; the viewer doesn't play it, so this is a free speed-up. --local implies it — nothing there consumes a clip |
| --fail-fast | off | if 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> | off | write the request body to a file instead of creating a draft; frames are still uploaded first when a key is present |
| --local <file> | off | write 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> | none | publish 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 renderorigin 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.
| Action | Target | What happens |
|---|---|---|
| open | path or URL | navigates; a relative path is appended to origin as-is, so it must start with / |
| click | selector | clicks the first match |
| type | selector + text | fills the field; the text goes last, in double quotes |
| check | selector | waits for the element to be visible |
| scroll | selector | scrolls the element into view |
| wait | selector or ms | waits; 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.
From draft to link
Capture, look, then publish — the summary is written from the frames, not from a diff:
Capture
walk shoots the frames, uploads them and prints the draft id. Nothing is public yet.
Look at the frames
This is where a wrong scenario gets caught, while re-shooting is still cheap.
Publish with a summary
Over MCP with publish_walkthrough, or with publish --summary. The link opens without an account and shows one step per screen.
The summary is all-or-nothing: leave it empty, or give exactly one line per step covering every step, with no step named twice. A line that can't point at a step doesn't belong in the summary — that rule is what keeps it from turning into filler. stepIdx counts from zero, so the first step is 0; numbering from one passes until the last line and then fails with “stepIdx must reference a step”.
The published page only shows. Viewers can't leave notes on a frame; to send feedback back they use the extension on the real page. See interactive demos for what the result looks like.
Sharp edges
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.
--localThe 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.
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.
Closing the window mid-run — yours or the one it launched — ends the run there. The steps captured so far still become a draft.
open plans look deadThe 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.