Script reference
Every section, key, step, target and modifier of a script. This page is the skill's own reference, references/script.md.
A script is YAML (JSON works too; in a Markdown file, the first fenced block tagged browser-clip is
the script, and it ends where CommonMark ends it: a fence indented 4 or more spaces past the opening
one, as in a card: | text, is text). Every key is optional except script. Omitted keys take the default below. Unknown keys
and out-of-range values are reported by validate, then ignored or clamped.
A script has two halves. The steps and the browser, capture and typing settings say what
happens in the browser: they make the take (the captured frames and event log). Everything else
says how the take is filmed, and changing it never captures again.
Contents: Shape · Top level · browser · capture · video · typing · cursor · camera · speed · Steps · Targets · Keys · Modifiers · Markers and time · Takes · Diagnostics · CLI
Shape#
# yaml-language-server: $schema=<path>/references/schema.json
version: 1
title: Create a project
platform: windows # terminal-clip's look: frame, backdrop, cursor
browser:
url: http://localhost:5173 # base for relative goto paths
viewport: 1280x800 # CSS pixels
camera: auto
speed: auto # compress page loads and waits
script:
- goto: /
caption: The projects page
- click: New project
id: new
- type: Launch plan
into: { label: Name }
- press: Enter
- wait: { text: "Project created: Launch plan" }
pause: 1 # a freeze in compose, not a wait in captureTop level#
| Key | Default | Meaning |
|---|---|---|
version | 1 | Format version |
title | "" | Name of the clip (outputs are named after the script file, or video.name) |
notes | — | Free text or a list (sources, intent). Not rendered |
platform | none | generic, windows, macos or linux: terminal-clip's look for that platform |
seed | 7 | Pointer paths, typing rhythm and typos. Each step has its own stream, keyed by its order and id |
browser, capture, video, typing, cursor, camera, speed, script | Sections below |
platform sets the defaults that browser.frame.style, cursor.style and video.background
would otherwise take, so that a browser clip cuts cleanly against a terminal-clip clip:
platform | Frame | Cursor | Backdrop (135°) |
|---|---|---|---|
| none | generic | macos | by browser.colorScheme: light #dde4ee → #9fadc2, dark #252b3b → #0d0f15 |
generic | generic | generic | #252b3b → #0d0f15 |
windows | windows | windows | #1d4a7a → #0a1426 |
macos | macos | macos | #4a3470 → #121225 |
linux | generic | generic | #4a1d3c → #140a14 |
browser#
What is captured. Everything here except frame is part of the take.
| Key | Default | Meaning |
|---|---|---|
url | — | Base URL for relative goto paths: http, https or file, e.g. http://localhost:5173. A path resolves against it as a link does: /settings from its origin, settings or ./settings under its own path. So with https://user.github.io/repo/, goto: / loads https://user.github.io/ and goto: ./ the repository's page (validate hints, H-URL) |
viewport | 1280x800 | WxH in CSS pixels (1280 × 800 works too): width 320–3840, height 240–2160 |
scale | 2 | Device pixel ratio at capture (1–4): zoom headroom for the camera |
colorScheme | light | light or dark (prefers-color-scheme) |
frame | The drawn browser window (not captured): see below. frame: macos sets the style |
frame:
| Key | Default | Meaning |
|---|---|---|
style | auto | generic, macos, windows or none; auto follows platform |
address | true | Draw the toolbar with the address bar (it shows the page's URL) |
tabs | false | Draw a tab strip with the page title |
capture#
| Key | Default | Meaning |
|---|---|---|
timeout | 10 | Seconds an action waits for its target (0.5–120); the timeout modifier overrides it per step |
clock | — | The page's clock when the capture starts, ISO 8601: 2026-10-12T09:30:00Z, or 2026-10-12T09:30:00+02:00. Without Z or an offset, the time is read in timezone (UTC without one). It then runs on, one clock for every page and frame of the capture: Date (and classes that extend it), Intl dates given no date, and Temporal.Now read it; workers keep the real clock |
timezone | — | IANA time zone for the page: Europe/Madrid, UTC |
mocks | [] | Requests answered from the script, so the page shows made-up data: see below |
block | [] | URL globs whose requests fail, e.g. "**/analytics/**": trackers, chat widgets, ads |
login | — | A login state file next to the script (<name>.storage.json): its cookies and local storage apply before the first page. See below |
The browser's language and locale are the machine's: number and date formats, and the
Accept-Language the page gets, follow it, so the same script captured on machines set to other
languages can differ. meta.json records it (locale); there is no setting for it yet.
A mock answers every request whose URL matches its glob (and its method, when it has one):
| Key | Default | Meaning |
|---|---|---|
url | required | URL glob: * stays within a path segment, ** crosses segments. /api/projects is that path on any origin, and //cdn.example.com/** that host on any scheme. Without a ? in the glob, the query string is ignored |
method | any | GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS |
status | 200 | HTTP status (100–599) |
file | — | A file next to the script, served as the body; the type comes from its extension |
json | — | An inline value, served as JSON |
body | — | Text, served as the body |
headers | — | Extra response headers: { X-Total-Count: "42" } |
delay | 0 | Seconds before the answer (0–30), to show a loading state |
- A mock has one of
file,jsonandbody, or none for an empty body. - Globs (of mocks and of
block) match the whole URL, so they start with/(a path),*or a scheme;validatewarns about any other (W-GLOB):api/usersorexample.commatch nothing. The scheme and host compare without case, the path with it. - Changed 2026-10-14: a glob that starts with
/used to match the end of any URL's path, as**/api/projectsstill does. Now/api/projectsmatcheshttp://localhost:5173/api/projectsbut nothttps://widget.example/v1/api/projects. Write**/…where the old meaning was meant. - Mocks answer CORS for the page, so an API on another origin needs no setup: they allow the
request's origin with credentials (
*for a request without anOrigin), let the page read the mock's ownheaders, and answer the browser's preflight (not counted as a mocked request). A mock'sheadersreplace these and the defaults, whatever the case of their names. - The first matching mock wins. A blocked request fails as
ERR_BLOCKED_BY_CLIENT. captureprints how many requests the mocks answered and how many were blocked, and warns about a mock that answered nothing (a wrong glob or method) and a block that blocked nothing.- Mocks, blocks and the bytes of mock files are part of the take: editing a mock file captures again.
A login state lets a capture start signed in, with no password in the script:
browser-clip login https://app.example.com/sign-in -o demo.storage.jsonopens a browser window with a fresh profile. Sign in there, then press Enter in the terminal. The command saves every cookie, and the local storage of each origin you visited, then closes the window.capture: { login: demo.storage.json }applies them before the firstgoto. The local storage is filled before the page's own scripts run, once per origin.
The file holds a live session, like a password:
- never commit it.
*.storage.jsonis git-ignored here, andloginwarns when git would not ignore the file; - prefer a demo account;
- sessions expire: run
loginagain (--forcereplaces the file).
A credential written in a URL would sit in the script, in the take and maybe in the drawn address
bar. validate warns (W-SECRET) when browser.url or a goto carries a user name and password
(user:pass@), or a query or fragment parameter named like a secret: token, access_token,
apiKey, key, password, secret, auth, sig… Sign in with a login state, or answer the
request with a mock.
Its bytes are part of the take, so a new session captures again. For scripts and tests,
--save-after <seconds> saves without waiting for Enter, and --headless hides the window.
video#
| Key | Default | Meaning |
|---|---|---|
format | wide | wide 1920×1080, short 1080×1920, square 1080×1080, portrait 1080×1350, or WxH (64–7680 px a side) |
res | 1 | Scale of the output (0.25–4): 2 is 4K for wide. The video stays within 8192 px on its long side |
fps | 60 | 24, 25, 30, 50 or 60 |
duration | auto | Seconds (0.5–3600). Shorter than the clip: fit: cut ends early, fit: speed speeds everything up (the global rate, from 0.1× to 8×; pauses, cards, the lead and the hold keep their length) |
fit | cut | cut or speed |
lead, hold | 0.6, 2 | Seconds on the first frame before the first step, and on the last frame after the last |
fadeIn, fadeOut | 0 | Seconds of fade from and to black (or transparency) |
background | auto | auto (see platform), a hex color ("#rgb", "#rrggbb", or either with alpha), "gradient #a #b 135" (hex colors, then an optional angle) or transparent |
padding | auto | Space around the window, px at 1080p: auto is 56, or 36 in a frame taller than wide |
shadow | true | The window shadow, as terminal-clip draws it (or 0–1) |
safe | auto | Area kept clear of platform UI: auto (from the format), none, or { top, right, bottom, left } fractions (0–0.45) |
captions | Caption style: see below | |
name | file name | Base name of the output files, next to the script (clips/demo makes a folder). No < > : " | ? *, which Windows file names cannot hold; an absolute path or .. gets a warning |
safe: auto keeps 9 % at the top, 6 % at the sides and 20 % at the bottom of a short frame, and 4 %,
4 % and 10 % of a portrait one; wide, square and WxH keep none. The window fits the padded safe
area, the camera centres on it, and captions and badges stay inside it. A viewport fills a vertical
frame when its shape matches: 720x1014 for short, 900x1019 for portrait, 960x916 for
square (with the address bar and no tabs).
captions, with terminal-clip's keys and defaults so both tools' captions match:
| Key | Default | Meaning |
|---|---|---|
position | bottom | bottom or top |
size | 44 | Text size, px at 1080p (12–160) |
color | #ffffff | Text color: hex (#fff, #ffffffcc), rgb(), rgba(), hsl(), hsla() or a CSS name |
background | rgba(0,0,0,0.62) | Box color, in the same forms |
font | sans | sans (Inter, bundled) or mono (a system monospace font) |
maxWidth | 0.86 | Widest line, as a fraction of the safe area (0.3–1) |
typing#
| Key | Default | Meaning |
|---|---|---|
wpm | 75 | Typing speed in words per minute (5 characters a word), 10–900; part of the take |
typos | true | Occasional wrong keys fixed with Backspace, or a frequency 0–1; part of the take |
sound | wav | The keyboard sound: wav (a recorded key), mechanical, soft or none; true is wav, false is none |
volume | 0.8 | Keyboard sound volume, 0–1 |
showKeys | false | Badges for pressed keys and for held keys while they are down. Typed text gets none |
The keyboard sound is terminal-clip's, at the times the take recorded: each typed or pressed key, and
a held key as it goes down and, softer, as it comes up. Keys 35 ms apart or closer make one sound
(never Enter). While the clip runs at 3× or faster only Enter and shortcuts sound, and sped-up keys
are quieter. The MP4 carries the track (AAC) unless the sound is none or the volume is 0, and
frames writes it as audio.wav.
cursor#
The synthetic pointer, drawn over the page along the paths recorded at capture. cursor: windows
sets the style.
| Key | Default | Meaning |
|---|---|---|
style | auto | macos, windows, generic or none; auto follows platform (macos without one) |
size | 1.5 | Scale of the arrow (0.5–3): 1 is the size of a real pointer on the page |
clickRipple | true | A ripple on clicks and on the press that starts a drag |
press | true | The pointer shrinks while the button is down (a click, the click into a field, a drag) and springs back: true (to 0.8 of its size), false, or a scale 0.5–1. Quick clicks never take it below that scale |
clickSound | false | A mouse click sound: the press and the release of each click, of the click into a field before typing, and of a drag. true (volume 0.6) or a volume 0–1 |
hide | 3 | Seconds at rest (video time) before the pointer fades out; it fades in again just before it moves. false keeps it on screen. Before its first move it counts as resting, so a clip played only with keys shows none |
camera#
camera: static sets the mode.
| Key | Default | Meaning |
|---|---|---|
mode | auto | auto, static, follow, push-in, pull-out, drift, handheld or manual |
shot | per mode | wide, window, medium, close or detail |
focus | action | action, cursor, page, window, or { step: <id or stepN> } |
zoom | — | Fixed zoom (0.3–8) instead of the shot's, for every mode but auto (the director picks one per shot; validate warns). push-in ends at it, pull-out starts from it |
lag | 0.35 | Seconds the camera trails what it follows (0.05–3) |
deadzone | 0.12 | Fraction of the view the target may move in without the camera moving (0–0.45) |
shake | 0 | Hand-held shake, 0–1 (handheld uses 0.35) |
clamp | true | A zoomed-in shot stays inside the window. The move between two shots is not held to it, so it never swings back |
move | smooth | Transition between shots: cut, smooth (1 s), fast (0.25 s) or whip (0.2 s, blurred while it moves) |
shots | [] | Explicit shots, see below |
The modes:
auto: the director. It opens on the window, frames each action box (a run of nearby actions shares one shot), shows the whole page for scrolls and held keys, and ends on the window. It holds through apauseor acard(they hold the step before) and sets off for the next step just before it.static: the whole window, all the time. Use it for README GIFs, overlays in an editor, and to keep a whole page in view in a vertical clip.follow: follows the action or the pointer.push-in,pull-out,drift: slow moves on the window.handheld:staticwith shake.manual: only theshotsyou write.
A shot in shots, or the camera modifier on a step. A step's shot starts just before the step and
lasts while it runs, its pause included; on a pause or card step, the whole freeze:
| Key | Meaning |
|---|---|
at | Marker where the shot starts: required in shots; a step's shot starts with the step |
shot, focus, zoom, move, shake | As above |
dur | Video seconds the shot lasts; then the mode resumes |
transition | Seconds the move into the shot takes |
rig | static, follow, push-in, pull-out or drift during the shot |
tilt | Degrees (−20 to 20) |
A shot can be written as its name (camera: close) or its move (camera: whip).
How shots meet:
- A shot that starts while the camera is still moving sets off from where the camera is, at its speed: the camera never jumps.
- A shot too short for its move is skipped, unless the next shot cuts, since the camera would only
set off and turn back: the director's (or the mode's) own when the next shot comes before its move
ends; yours (
shots, a step'scamera) when it comes before its move is half done, withW-SHOT. Start it earlier, or make its move quicker (move: fast,transition). focus: actionframes the action box of the step under way when the shot arrives (after its move), else of the last step before it, whose result is on screen. A box out of view (awaitthat found its text below the fold) is not framed: the director leaves it out, and a shot that names its step frames the pointer.
speed#
speed: 2 sets the rate, speed: auto turns auto on, and a list sets the ramps.
| Key | Default | Meaning |
|---|---|---|
rate | 1 | Global playback rate (0.1–8). Ramps, step speeds and the automatic speed-up multiply it, as does the rate fit: speed finds |
auto | false | Compress the spans a viewer only waits through (a page load, a wait, an action waiting for its target) when they last over over seconds (3), down to to seconds (1.5); never a hover's hold. true or { over, to } |
badge | auto: true | Show "⏩ 6×" while sped up, with the rate on screen; text may use {rate}. On by default for automatic speed-ups (up to 32×), off for ramps and step speeds |
ramps | [] | { during: <marker range> } or { from, to } markers, with rate (required, 0.05–32), ease (seconds, 0.4) and badge |
A ramp or step speed ends by easing back to the global rate. When the next one starts where it ends
(the next from is its to, or the next step follows), it hands over at its own rate instead:
2, 4, 8 play as a staircase that never drops back to 1× in between, and their badges change
number in place.
Pauses, cards and the lead inside a ramp or step speed play: the picture holds, then goes on at the ramp's rate, and the badge steps aside meanwhile.
One inside another plays at its own rate, and the outer one resumes after it: a ramp from start
to end at 2 with speed: 4 on a step plays that step at 4×, the rest at 2×. Two that overlap
only partly are trimmed: the later one starts where the earlier one ends (W-RAMP). A range past
the clip's ends is clamped to them (W-RAMP). during spans a step that takes scene time: not
start or end, not a pause, caption or card step.
Steps#
Each step is a map with one action. The first nine run in the browser; pause, caption and
card exist only in compose and never change the take.
| Action | Value | Fields |
|---|---|---|
goto | URL (http, https or file; about: and data: too), or a path resolved against browser.url (see browser) | typed: first show the URL typed into the drawn address bar (video only): true, or the text to type |
click | target | at: [x, y]: a point in the target's box, as fractions (0–1) |
hover | target | hold: seconds the pointer rests (1); at |
type | text, or { text, into, wpm, typos } | into: a target clicked first; wpm, typos for this step. A line break in the text is inserted as text, not pressed as Enter (use press: Enter for that). A visible text field that does not end with the typed text afterwards (a mask, a length limit, a number field) gets a warning from capture |
press | a key or a list of keys, pressed in turn | times: repeat (1–100) |
hold | a key or a list of keys held together | for: seconds down (required); repeat: the last ordinary key auto-repeats after 0.5 s, 30 a second (true) |
drag | target | from: [x, y] ([0.5, 0.5]), to: [x, y] (required), for: seconds the move takes (0.8) |
scroll | pixels (negative scrolls up), { to: <target> } or { by: <pixels> } | Pixels and by turn the wheel over whatever is under the pointer. to turns the target's own scrolling area (a panel of an app shell, or the page), the pointer first moving over it when it rests elsewhere, until the target sits near the area's top; a target still out of view afterwards gets a warning |
wait | seconds, or { text }, { css } or { js: <expression> }, with gone: true to wait until it goes | |
pause | seconds | A freeze on the frame that shows the previous step's result |
caption | text, or { text, dur, position } | dur: video seconds on screen; position: bottom or top |
card | title, or { title, subtitle } | title, subtitle, background, color (colors as for captions), align (center or left), dur (2.5) |
Details:
- Every action takes a value.
- wait:or- caption:alone is an error, and so is an emptypressorholdlist;caption: ""ends a caption. A card'stitleandsubtitlemay sit next tocard:instead. A script needs at least one step, and agoto: without a page, capture would film a blank one. typedon agoto: before the page, the video shows its URL typed into the drawn address bar. The camera moves to the bar, the keys sound, Enter brings the page (the bar keeps the URL while it loads), and the camera pulls back to the window. On the firstgotoit starts from an empty browser; on a later one, from the page before. It is compose only: the take does not change.truetypes what the bar shows for the URL (example.com/pricing); a string types that text. It needs the address bar (frame.address). A URL longer than the field shows its end while it is typed, as a browser scrolls the field.- The address bar shows a URL as a browser does: without
https://, and with an international host and the escaped UTF-8 of a path, query or fragment as their characters (müller.de/über, notxn--mller-kva.de/%C3%BCber). Escaped ASCII (%20,%2F) and invisible or reordering characters stay escaped. wait: { js }takes one JavaScript expression, which may useawait; the step waits until it is true (withgone, until it is false). Expose the state a script needs in the page you film, e.g.window.game.ready. An expression still unsettled at the step'stimeout(an awaited request the server holds) fails the step then.holdpresses the keys in order, keeps them down forforseconds, then releases them in reverse order. A modifier may be held alone:hold: [Shift, w].dragmoves tofrom, presses, moves totowith the button down along an eased path, and releases.caption, as a step, shows from where the previous step ended until the next caption, itsdur, or the end of the video. Two caption steps in a row switch without a fade;caption: ""ends the one on screen. Next to an action it is the modifier.cardholds the page fordurseconds under a full-frame title card. A card first in the script opens the video, from its first frame. Withalign: leftits lines start at the left margin.- Caption and card text breaks at spaces and at line breaks in the text (
"Line one\nLine two", or a YAML block). A word too long for a line (a URL, a CJK sentence) breaks where a browser would: between its words, after/or&, or between characters. - Caption and card text takes terminal-clip's markup:
[bold]Done[/],[#8ab4ff]Projects[/],[green]ok[/],[color(33)](the 256 colours),[default](the text's own colour). A background ([white on #202020]) is accepted and not drawn: the caption has its box. - Pointer lock (mouse-look) does not work in headless capture.
Targets#
How an action finds its element, in the order to prefer:
| Form | Finds |
|---|---|
Save (a string) | An element whose visible text is Save (exactly, else containing it), lifted to its button, link or label; or a button <input> whose value is Save. For an accessible name (an icon button's aria-label), use { role, name } or { label } |
{ role: button, name: Save } | An element by ARIA role and accessible name |
{ label: Email } | A form field by its label |
{ text: Saved } | An element by its visible text |
{ testid: save-button } | data-testid="save-button" |
{ css: "#dialog-close" } | A CSS selector: the escape hatch |
Before acting, the step waits up to its timeout until the target exists, has been scrolled into
view, has stopped moving, and is not covered at the point the action uses. A map uses exactly one of
these keys (name goes with role), and none of them may be blank. A target that never gets there
fails its step with the reason: not found, not displayed, zero size, hidden, moving or covered. A
control found through its <label> that cannot take the click itself (a checkbox drawn by its
label, the input visually hidden) is clicked through the label, as a person would.
A selector the browser cannot parse fails its step at once ("invalid CSS selector"): other tools'
syntax, such as Playwright's :has-text() and text=, jQuery's :contains() or XPath, and an id
that starts with a digit (#123; write [id="123"]). validate hints about them (H-CSS).
Keys#
For press, hold and the badges:
- a character:
a,A,7,/; - a named key:
enter,tab,escape,backspace,delete,up,down,left,right,home,end,pageup,pagedown,insert,space,f1–f12. Case does not matter;ArrowLeft,Return,Esc,DelandBsare aliases; - a combination:
ctrl+a,shift+tab,alt+f4,meta+k(cmdismeta). Withshift, a letter is its capital, as on a keyboard:shift+gsendsG; - in
hold, a modifier alone:shift,ctrl,alt,meta.
Badges name every modifier of a combination: Ctrl+Shift+A, or ⌃⇧A on platform: macos; meta
and cmd are ⌘.
Modifiers#
On any step:
| Key | Meaning |
|---|---|
id | A name for markers: lowercase letters, digits and _, starting with a letter. Not start, end or step1, step2…, which markers already mean |
pause | Seconds of freeze after the step |
timeout | Seconds this step waits for its target (0.5–120) |
speed | Playback rate while the step runs (0.05–32) |
camera | A shot while the step runs (see camera) |
caption | A caption while the step runs (its pause included), for at least 1.2 s; or { text, dur, position } |
comment | Anything; ignored |
pause and caption next to another action are modifiers; hold next to hover is the rest time.
Markers and time#
A marker names a moment: start, end, a step (step3, or its id) and one of its events, plus an
offset in seconds: save, save.act, save.end+0.4, step2.settled-0.2. A step without an event
means its start, except in a speed ramp's during, which spans the step from start to end, and
to, where it means the step's end: { from: load, to: save } speeds up both steps.
| Event | When |
|---|---|
start | The step begins (the pointer starts moving, the wait starts) |
act | The click, the first key or the wheel event is dispatched |
settled | The page has settled after the action |
end | The step is over |
Markers anchor camera shots (at), speed ramps (during, from, to), and the --at and
--range options. Scene time is the take's own time; video time is what the viewer sees after speed
changes, pauses and cards. Offsets and numbers under camera: and on the command line are video
seconds; offsets in speed ramps are scene seconds.
Next to pauses and cards, a marker means the same moment however the capture's frames fell:
- A step starts after the pauses, cards and lead before it, and its
settledandendcome before its ownpause:save.endis the moment the pause aftersavebegins. - A
pauseorcardstep spans its freeze:startandactwhen it begins,settledandendwhen it ends. Acaptionstep is where its caption appears. - A
gotowithtypedstarts as the typing starts; itsact,settledandendcome after Enter, before the lead. - A speed ramp leaves out the pauses and cards at its ends: one
from: save.endstarts after the pause aftersave.
info lists each step at the times of its markers.
Takes#
capture, or the first command that needs a take (info, render, gif, sheet, still,
frames), runs the script in headless Chrome, Edge or Chromium. It stores the take in
.browser-clip/takes/<key>/ next to the script: the frames (JPEG), events.json (each action's
times, boxes, pointer paths and keys), meta.json and snapshots.json.
- After each captured step, capture records the elements on screen: controls, links, fields,
canvases, elements with a test id, and text. Each has its box and the targets whose first match
it is, checked in the page with capture's own lookups: its text, a label, a role and name (for a
control with no text), a test id, a CSS selector.
targetslists them, and the studio's picker uses them. The video leaves out the time this takes, and the time the browser stalled on an input (capturereports such stalls). - A snapshot keeps at most 400 elements: the controls first, then text.
targetssays when a snapshot was cut (a page of long text). - A take captured before snapshots existed has none:
capture --forcerecords them. - The key hashes what the take depends on:
- the captured steps, with their capture fields,
id(it keys the step's random stream) andtimeout. A value written out as capture would use it anyway (times: 1, a step'swpm,typosortimeoutequal to the script's) is left out, so it keeps the take; browserexceptframe,capture(with the bytes of mock files and of the login state),typing.wpm,typing.typosandseed.
- the captured steps, with their capture fields,
- Presentation edits reuse the take:
video,camera,speed,cursor,browser.frame,typing.sound,typing.volume,typing.showKeys, thepause,captionandcardsteps, and thepause,speed,cameraandcaptionmodifiers. --recapturecaptures again. Never commit.browser-clip/.- Old takes are pruned. A take names its script in
meta.json(script), and each new take removes from.browser-clip/takes/the takes nothing needs any more. Each script there keeps its 3 newest takes (a take kept after a failing step counts, until its key is captured) and the take it needs now; a take from before scripts were named stays only while a script there needs it. A capture in progress in another process is never touched, nor anything else in the folder.prunedoes the same on request (--keep <n>). A take that went is captured again when it is needed. - The take stops at the first step that fails, and compose refuses a take that stopped early. A
page that crashes or a browser that exits fails the step it was on, and so does a page that has
not answered for the step's
timeoutplus 2 s (a script that never ends). - Some steps succeed with a warning, which
captureprints andmeta.jsonkeeps: a page the server answered with an error status (404, 500…), which the video shows, and a typed text the field does not end with. - The address bar follows the page: each document it loads, and each route a single-page app sets
with the history API (
pushState) or a hash. Agototo a hash route of the open page loads no document, so it waits for none. The tab shows the page's own title, never a frame's or another tab's. - After each step, capture waits for the network to go quiet. A stream (
EventSource) never counts, nor a request still open after 5 s (a long poll, a video);capturelists those. A check that lands on a navigation (a sign-in that moves on) runs again in the next page. - A
waitfortext,cssorjsends once the screencast has sent a frame that shows the page in that state (at most 0.4 s later; when the page is at rest, the video leaves that time out), so apauseorcardright after it holds the result. - Capture accepts every JavaScript dialog (
alert,confirm,promptwith its default text, and the question a page asks before you leave it), records it in the take and lists it in thecaptureoutput. The video does not show native dialogs: film the result of the choice. - Capture films one tab. A click that opens another (
target=_blank,window.open) gets a warning, the filmed tab is brought back to the front, and a step that fails later names the new tab: to film that page,gotoits URL. Downloads are denied (a take keeps no files, and nothing is saved on the machine);capturelists them. - Frames and events are timed from one moment, read on both of the machine's clocks when the capture starts. A take whose frames do not fit that time (the system clock changed during the capture) gets a warning: capture it again.
- The studio never captures on its own: it shows the last take until you ask (its button, or
ctl capture).
Diagnostics#
validate prints file:line:col severity CODE path: message (fix), for example
demo.yaml:6:5 warning W-KEY script[2].gone: "gone" beside wait: is ignored (put it inside wait:, e.g. wait: { …, gone: true }). The path is left out for the YAML text itself, and the fix when there is
none. --json gives them as data.
| Code | Meaning |
|---|---|
E-YAML | The text is not valid YAML (or JSON) |
E-TYPE | A value has the wrong type or form: a viewport that is not WxH, a bad URL, clock or time zone, a missing required field, a js wait that is not one expression, an action with no value, a script with no steps, a color or backdrop the renderer cannot paint, a video.name Windows cannot hold, an override path that is not a setting |
E-ACTION | A step has no action, or two |
E-TARGET | A target does not use exactly one way to find the element, or a value of it is blank |
E-KEY | An unknown key name |
E-REF | A marker, focus or id that names no step; an id that markers already mean (start, end, stepN); a relative goto without browser.url; a mock or login file that is not next to the script |
E-ORDER | An action before any goto, or a script that never loads a page |
E-DUPID | Two steps with the same id |
E-TAKE | The take does not match the script, or it stopped at a failing step |
E-LOAD | In the studio: the loader failed on the text, a bug in browser-clip; the message says what failed |
W-KEY | An unknown or misplaced key, ignored (with "did you mean"), a card with no text, or a video.name outside the script's folder |
W-RANGE | A value outside its range, clamped |
W-RAMP | A speed ramp too short for its ease, overlapping another, past the clip's ends, or with nothing to speed up (during: start, during over a pause, from or to next to during) |
W-TIME | video.duration with fit: speed needs an implausible rate, or cannot be reached at any |
W-SHOT | A camera shot (in shots, or a step's camera) that the next one replaces before its move is half done; skipped |
W-GLOB | A mock's or block's URL glob that matches no URL: it starts with neither /, * nor a scheme (api/users, example.com); the fix shows the glob as a path or a host |
W-SECRET | browser.url or a goto carries a credential: user:pass@, or a parameter such as token, key or password. Use capture.login or a mock |
W-YAML | A YAML parser warning |
H-YAML | Text YAML would misread, quoted for you: the value of a text key (click, type, text, caption, title…) with ": " or a final ":", or that starts with "- " or "? "; the fix shows it quoted. Or yes/no-like text, which YAML 1.2 reads as text. Where a field takes true or false and also text or a number (typed, badge, typing.sound, cursor.hide…), a plain yes, no, on or off is read as true or false; quote it for the text |
H-LOGIN | A login file not named <name>.storage.json, the name .gitignore excludes |
H-CSS | A css target or wait the browser cannot parse: Playwright's :has-text() or text=, jQuery's :contains(), XPath, an id that starts with a digit. Capture would fail the step |
H-URL | A goto path from / under a browser.url with a path of its own: it loads from the origin, outside that path; the fix gives ./path |
Exit codes: 0 ok, 1 errors or failure, 2 usage, 3 warnings with --strict, 4 environment (no
browser found, or it would not start: the error shows the last lines it wrote), 130 Ctrl+C, 143
SIGTERM. A command stopped by Ctrl+C or a signal closes its browser and removes its temporary
folders.
CLI#
node <skill>/scripts/browser-clip.mjs <command> [script] [options]| Command | Does |
|---|---|
examples | Lists the bundled example scripts |
init [file] | Writes a commented starter script (--url, --platform, --format; the viewport fills the format), or copies an example (--from <name>). Either starts with the schema modeline. --force overwrites |
validate <script> | Checks the script (alias check); --json, --strict, --quiet (no hints) |
capture <script> | Captures the take, or reuses a cached one; --force captures again. After a new take it prunes .browser-clip/takes/ and prints what stays and what went |
prune <script> | Removes the takes next to the script that nothing needs any more: each script there keeps its 3 newest (--keep <n>; 0 keeps only the takes the scripts need now) and the one it needs now, the given script with the command's overrides (Takes). A folder works in place of a script |
info <script> | Duration, steps, camera shots, speed segments, overlays and the sound, in video seconds; --json adds every marker |
targets <script> | The elements on screen after each captured step and the targets that find them, best first: --after <step> (a number or an id), --json |
render <script> | MP4 (H.264 by default) with the sound track (AAC, or Opus where AAC is missing): --preview (half size, 30 fps), --fps, --range a..b, --codec h264|vp9|av1, --bitrate 2M, --deterministic (software VP9, identical files; AV1 where VP9 is missing). A warning says when the browser cannot encode the codec asked for and the MP4 uses another, or when a deterministic render gets H.264, which is not reproducible |
gif <script> | Animated GIF, silent: --fps (15, at most 50: browsers show frames shorter than 20 ms for 100 ms), --width (960), --range a..b |
frames <script> -o <dir> | PNG sequence for a video editor, and the sound track as audio.wav (16-bit, 48 kHz) unless the clip is silent: --transparent, --fps, --range, --scale. The new sequence replaces the files of an earlier one only once it is complete (frames past its end go, and so does browser-clip's audio.wav when the clip is silent). An audio.wav that browser-clip did not write (a voice-over) is never replaced or removed: with a sound track, frames stops before any capture |
still <script> --at <t> | PNGs at seconds or markers (--at 2.5,save.act,end), with --transparent, --scale, --director (the whole frame with the camera's view drawn on it), --no-camera; without --at, the contact sheet |
sheet <script> | A contact sheet PNG of key moments: --max 12, --sheet-cols, --width |
schema | The JSON Schema of scripts (shipped as references/schema.json); -o writes it, making its folder |
login <url> -o <name>.storage.json | Opens a browser window to sign in by hand; Enter saves the session for capture.login. --save-after <s>, --headless, --force |
studio <script> | Opens the studio, a local page bound to the file: the live preview, the YAML editor and its diagnostics, forms, a timeline and an inspector. Exports go next to the script, named as render names them (video.name, or the script's name; a folder in it is made), and show what the script says: the text on screen, its take and its camera. The studio writes inside the script's folder only: a video.name that leaves it is refused there (render it with the CLI). Its picker (⌖, P) adds a step for an element clicked on the preview, with the targets from the take's snapshots. Edits there are saved to the file; edits to the file appear there. An edit that needs a new take is captured on request (the page's button, or ctl capture); until then the timeline only seeks. A missing mock or login file shows in the take banner and the diagnostics. A save from one open page appears on the others. A script that is not UTF-8 (a Windows-1252 or UTF-16 file) is shown but never saved over: save it as UTF-8 first. --port (a port in use stops it at once, exit 1), --no-open, --camera <mode> (the preview only, like its A/B compare); other overrides are refused. One studio per script (a second one exits 4). Ctrl+C stops it |
ctl <script> <command> | Drives the open studio's page (with several pages open, the one opened last): state (JSON, with sound: whether the page may play sound, and whether it does), seek <t>, still <t> (-o, default <script>-studio.png next to the script; --scale, --director; with the script's camera, as still), play, pause, reload (reads the file from disk), capture (waits as long as the capture may run, 10 minutes), camera [mode] (the preview only; no mode: the script's). The preview plays the sound only after a click or a key press on the page. Exit 4 when no studio answers for the script |
Options for every command:
-o, --out <path>: the output file (default: next to the script, named after it);-o<path>works too;--recapture: capture again before drawing;--camera <mode>,--platform <p>,--format <f>,--url <base>,--viewport WxH,--seed <n>,--sound <s>(typing.sound);--<section>.<key> <value>and--set <path>=<value>override any setting, e.g.--video.fps 30or--set browser.colorScheme=dark. A number indexes a list:--set camera.shots.0.zoom=2.
Numbers (--fps, --scale, --width, --max, --sheet-cols, --port, --save-after,
--keep), --codec, and a gif --fps above 50 are checked before anything runs (exit 2).
--scale is at most 4, and a still or a frame at most 16384 px a side (the most the browser
encodes), checked before any capture. render, gif, still, sheet and frames check their
output files before any capture: a folder in a file's place, or a name Windows cannot hold, stops
the command. Each file is written whole: to a temporary name next to it, then renamed. An MP4 is
written there as it is rendered, so a long clip never waits in memory whole.
The script - reads standard input.
Environment:
CHROME_PATH: the browser's program file (chrome.exe,msedge,chromium…), used instead of searching the usual places. A path that is not a file is an error (exit code 4).BROWSER_CLIP_NO_SANDBOX=1: run the browser without its sandbox, for a container that cannot provide one (the browser then exits at once, saying "No usable sandbox"). Run as root, the browser always goes without it.
Source: docs-src/pages/reference/script.md · browser-clip 0.1.0