Point your coding agent at your web app. Get a narrated, captioned, verified demo video.
npx skills add samugit83/rushes -g --all -yRushes turns a web application into a demo video. You describe the demo, your coding agent writes a storyboard, and Rushes drives your real UI in a real browser, narrates each scene, and cuts it together with captions and chapters. Then it checks the result: every step resolved, every scene shows what the voice claims, no dead air, no secrets on screen. If a check fails, nothing is delivered. Works with Django, Rails, Next.js, or a static site. Two API keys, both for the voice.
One path. Every other command lives at the bottom, under Everything else; you do not need any of them to get a video.
You will not be typing rushes commands. You install it once, then you talk
to your coding agent and it runs them. Your part is answering five questions and
looking at four things.
npx skills add samugit83/rushes -g --all -yPrefer to work from a clone? Development, or any change you want to make to the skill itself, starts here instead:
git clone https://github.com/samugit83/rushes.git ~/rushes && cd ~/rushes && npm install npm link # gives you `rushes` mkdir -p ~/.claude/skills && ln -s ~/rushes ~/.claude/skills/rushesEverything from Step 2 onward is identical either way.
-g makes it global and --all answers every prompt, so it does not matter
which directory you are in and rushes is available in every project on your
machine. You never run this again. It lands in ~/.agents/skills/rushes, with
~/.claude/skills/rushes pointing at it.
Two lines near the end will say Eve does not support global skill installation
and the same for PromptScript. That is expected: those two agents have no
global location, and every other agent installed fine.
Then give yourself the command the rest of this README uses:
alias rushes='node ~/.agents/skills/rushes/bin/rushes.mjs'Put it in your ~/.bashrc or ~/.zshrc. Your coding agent does not need this,
it runs the entry point by path, but every rushes ... below is written for a
human at a shell.
Why there is no installed binary. Rushes runs its TypeScript directly, with no build step, because Node strips the types at load time. Node deliberately refuses to do that for files under
node_modules/, sonpm install -ggives you arushescommand that dies withERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING. A skill directory is not undernode_modules, which is why the alias works and an npm install does not.
The first run installs the skill's own dependencies, once, into its own directory. Nothing else to do.
rushes setupIt checks what is needed, installs what it can, and tells you the rest:
checking what Rushes needs
✓ node v22.11.0
· ffmpeg not found
· browser not found
installing the browser, into a cache in your home (no privileges needed)
npx --yes playwright install chromium
✓ done
ffmpeg is a system package, so this one is yours to run:
sudo apt install -y ffmpeg
Rushes will not run a privileged command on your behalf: you asked it to
set itself up, which is not the same consent as installing system packages.
Copy that one line, run it, and run rushes setup again. It will say:
✓ everything Rushes needs is installed.
Why it installs one and not the other. The browser goes into a cache
directory in your home and needs no privileges, so setup fetches it. ffmpeg is a
system package, and a tool that runs sudo because you typed an unrelated
command is executing things you never read. The command it prints is the exact
one for the package manager you actually have, not a guess from your OS family.
(rushes doctor is the read-only version: it reports and changes nothing.)
Two keys from ElevenLabs, exported like any other
environment variable. Put them in your ~/.bashrc or ~/.zshrc so you never do
this again:
export ELEVENLABS_API_KEY=sk_your_key_here
export ELEVENLABS_VOICE_ID=your_voice_idNothing else is ever a secret you give to Rushes. Your application's own password or token is never stored here, see Safety and privacy.
cd ~/code/my-app
claudeYour app should be running, the way you normally run it.
Type this into the chat. That sentence is what starts everything:
make a demo video showing how the reports page works
That is genuinely enough — the agent asks what it needs and discovers the rest by looking at your app.
When you already know the story you want to tell, say so. Nothing below is a special syntax; it is a paragraph, and every line of it maps onto something the tool actually does.
make a demo video of our app. here is the shape I want:
1. open with a slide that introduces the product: the name, one line on what
it is for, and four boxes for the pieces a new user meets first -
projects, reports, exports, alerts. keep it calm and let the voice carry
the detail.
2. then film the real app. start on the dashboard, walk through the filters
panel, then the saved views on the right, and then actually create a new
project called "Q3 rollout" and land on its empty state.
3. after that, a slide with an animated diagram of how the thing is put
together. read the code in ./services and ./web to work out the real
architecture - do not guess it - then draw it and narrate the path a
request takes, lighting up each box as you name it.
4. back into the app: open the reports page, filter to last month, sort by
owner, and export a CSV so the download lands on screen.
5. then visit https://docs.example.com/reports/exports, read it, and narrate
what the export limits actually are, in your own words.
6. then open the alerts section. I genuinely do not know what is worth
showing there - explore it yourself, find two things a developer
evaluating us would care about, do them, and explain them.
7. close on a slide: one sentence on who this is for, and where to start.
about four minutes, for a developer evaluating us.
| What you asked for | What Rushes does with it |
|---|---|
| 1. an intro slide | a composed slide using the flow-row block. The palette and typeface are extracted from your running app, so it does not arrive looking like a stock template. Every slide is measured after rendering: text below the size floor, contrast under 4.5:1, anything outside the frame, and text too small once projected into a 640px player all fail |
| 2. navigate, interact, create | live scenes driven by visible text and ARIA roles rather than CSS classes, so they survive a refactor. The create step gets an expect on the empty state — a video of a form that never submitted cannot be delivered |
| 3. a diagram from the real code | the agent reads the folders you named and drafts a slide with a topology, not a picture. Connectors are drawn after layout from the measured boxes and then checked: a route through a box it does not connect is a hard failure at every profile |
| 3. "lighting up each box as you name it" | slide beats, anchored to a word in the narration. The voice provider returns character-level timings, so focus, reveal and travel fire on the word — and a reworded sentence, or a different voice, re-syncs every beat with no edit. A beat that never fired fails the build at the publishable profile, rather than being something you notice on the fourth viewing |
| 4. filter, sort, export | more live scenes. If the narration says a number, bind it with an assert and it is checked against live data before recording — a figure that was true when you wrote it and false when it filmed is the failure that cannot be fixed after upload |
| 5. read an external page | an off-origin visit. The host must be listed in external.allow, it is filmed from a fresh browser context that never held your credentials, only a picture crosses back, and the source URL stays on screen. Mark the scene volatile: someone else's page rotates its content, and the rehearsal must not demand it be identical twice |
| 6. "explore it yourself" | rushes discover walks the section and drafts scenes whose expects are pre-filled from elements it actually saw. It is a proposal, never a recording — you approve the outline at Gate 2 first |
| 7. a closing slide | a final slide scene plus the outro card, both on the same palette |
| "about four minutes" | scene pacing, and the advisory score sheet afterwards: mean scene length, motion share, reading rate, dead air |
You still get the five questions, and it still stops at the four gates. A longer prompt changes the storyboard — never the checks, and never what may be published without your say-so.
The agent asks all five at once, then does the work. Here is a real exchange:
You make a demo video showing how the reports page works
Claude Five things before I start:
1. What should a viewer be able to do or understand when it ends?
2. Where is the app, and is it already running?
3. How should I sign in?
- it is public, no sign-in
- I will sign in myself once, now <- recommended
- there is a login form; here are throwaway credentials
- a token from an environment variable I already export
4. Which account or tenant should I record, and is everything
visible in it safe to publish?
5. How long, and who is watching?
You 1. that they can filter by month and export a CSV
2. http://localhost:8000, it is running
3. I will sign in myself
4. the demo tenant, yes it is all seeded fake data
5. about 3 minutes, for a developer evaluating us
Claude I need you to sign in once. Run this and log in as you normally
would - I never see your password:
rushes login
You (a browser opens, you log in, you press Enter, done)
ok done
That rushes login is the one command you type yourself, and only if your
app needs a sign-in. A browser opens, you log in however you normally do (password
manager, 2FA, SSO), and the session is saved. Every later recording is
unattended.
The agent stops and waits at each of these. Nothing happens until you reply.
Claude GATE 1 - here is what I understood:
app http://localhost:8000, signed in as demo@acme.test
palette extracted from your app: bg #0f1117, accent #4ade80,
font Inter
shape demo-led, 7 scenes, ~3 min, dark, calm explainer
assuming 1920x1080 at 30fps, captions as sidecars,
publishing OFF
Correct anything.
You good, but say "monthly report" not "report page"
Claude GATE 2 - the outline:
1 [live] the reports list
S1 [composed/flow-row] how a report is built: 4 steps
2 [live] filtering by month
3 [live] exporting a CSV
Approve, or tell me what to change.
You approved
Claude GATE 2.5 - here is every slide as a picture, before I spend
anything: ./out/reports-tour/slides-preview/contact-sheet.html
You slide 1 is too dense, drop the last box
Claude Done. Building now - this takes about 8 minutes.
Then it works. Voice, browser, recording, checks.
Claude GATE 3 - done. 21/21 showcase, 0 errors, 1 warning.
video out/reports-tour/reports-tour.mp4 (3m04s)
thumbnail out/reports-tour/reports-tour.thumb.png
frames out/reports-tour/evidence/contact-sheet.html
captions out/reports-tour/subtitles/reports-tour.vtt
rehearsal: agreed. frame evidence: passed.
7 scenes verified against the narration.
NOT published. Watch it, then tell me if you want it up.
Your video is in your project, under out/<demo-id>/:
xdg-open out/reports-tour/reports-tour.mp4 # Linux
open out/reports-tour/reports-tour.mp4 # macOS
start "" out\reports-tour\reports-tour.mp4 # WindowsWhat is in that folder:
| File | What it is |
|---|---|
reports-tour.mp4 |
the video |
reports-tour.thumb.png |
a 1280x720 thumbnail |
subtitles/*.vtt and *.srt |
captions, never burned into the picture |
reports-tour.youtube.txt |
title and description, with chapters |
reports-tour.linkedin.txt |
a ready-to-paste post |
evidence/contact-sheet.html |
one frame per scene, from the delivered file |
receipt.json |
what was checked, and the hashes binding it to the storyboard |
This is a finished outcome. If you only wanted a video, you are done.
Only if you want to. Publishing is absent until you set it up, and it takes three things, once.
9a. Get a Google OAuth client. In the Google Cloud console: enable the YouTube Data API v3, then Create credentials → OAuth client ID → Desktop app, and download the JSON. Put it in your project:
mkdir -p credentials
mv ~/Downloads/client_secret_*.json credentials/client_secret.json9b. Authorise once.
rushes publish-authIt prints a URL, you approve in the browser, and the token is saved.
9c. Tell your config where videos go. Add this to rushes.config.json:
9d. Then just ask. In a new message. This is deliberate: approving the video is not approving an upload.
You publish it, unlisted
Claude Uploading reports-tour, privacy unlisted.
✓ uploaded: https://youtu.be/dQw4w9WgXcQ
set custom thumbnail
uploaded caption track (en)
LinkedIn post ready with the real link:
out/reports-tour/reports-tour.linkedin.txt
The upload refuses unless all five hold: a receipt exists, it was produced at the showcase profile, it recorded zero errors, and both the storyboard hash and the video hash still match the bytes on disk. Edit the storyboard after recording and the publish is refused, because the chapter timestamps would no longer match the video.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#151320','primaryTextColor':'#e7e5ea','primaryBorderColor':'#8b8aa0','lineColor':'#f59e0b','secondaryColor':'#1d1a2b','tertiaryColor':'#0b0a10','fontFamily':'system-ui'}}}%%
flowchart LR
A[storyboard<br/>scenes, steps, expects] --> B[voice<br/>sets the clock]
B --> C[record<br/>your real browser]
C --> D{check<br/>the finished file}
D -- pass --> E[deliver<br/>atomic + receipt]
D -- fail --> F[diagnostics<br/>with supportedFixes]
F -.-> A
E -.-> G[publish<br/>optional, receipt-gated]
The loop from check back to the storyboard is the part that is different. Every
failure is structured data, a stable code, the exact subject, the measured
evidence, and an enumerated list of repairs, so the agent that wrote the
storyboard can converge instead of guess.
Yes. It will work, and you will get a file. The difference is everything that happens after the file exists.
A script tells you it succeeded when the script ran. It says exactly the same thing when:
- every click missed and you filmed a page that never changed
- the app was signed out and you filmed a login screen for ninety seconds
- the audio track came out silent
- a scene is a black rectangle because a route 404'd
- a customer's API key was on screen in scene three
Rushes opens the finished mp4 and measures it. If any of the above happened, nothing is delivered and it tells you which one.
Here is the same job, both ways.
| a hand-written script | Rushes | |
|---|---|---|
| "did it work?" | the script exited 0 | the finished mp4 was measured |
| waiting for the page | waitForTimeout(2000) |
waits until nothing is loading, no animation is running and the app's own spinner is gone, and names the one that never happened |
| narration length | you guess, so it gets cut off or leaves dead air | the voice is made first and measured; the picture is held to it |
| the mouse cursor | invisible, because headless records the page, not the desktop | drawn, and glided onto each target |
| a step breaks | a stack trace | the scene, the step, a screenshot of that moment, the three closest strings that were on screen, and a list of fixes known to work |
| a bad run | overwrites your last good video | builds in a temp folder; a failure leaves the good file untouched |
| you edit one caption | re-record everything | re-cut from the existing recording, no voice re-billed |
| your UI changes next month | you find out from a viewer | it tells you which published videos are now out of date |
- Proof, not hope. Every scene declares what must be on screen when it ends. If it isn't, the build fails. A script has no idea what "correct" looks like.
- A rehearsal. It runs the whole thing twice, silently, before recording. If the two runs disagree, it refuses to record — so a flaky app costs you nothing instead of a wasted take.
- The voice is the clock. Narration is synthesised and measured first, and every scene is held at least that long. A sentence can never be cut off.
- Real waiting. Not a sleep. Readiness is a measurement, which is also why the same engine films Django, Rails, Next.js and a static site with no framework detection anywhere.
- A repair loop that converges. Failures come back as structured data with an enumerated list of repairs, so your agent picks a fix instead of guessing — and it knows when to stop instead of looping.
- Safety you would not think to add. It won't hand your login credentials to a third-party site, won't reach an internal address or cloud metadata by accident, scans every scene for keys and tokens on screen, and refuses to run a start command from a config file without you typing yes.
- Everything from one source. Subtitles in both formats, chapters, a thumbnail, a description and a social post, all consistent with the cut.
- A receipt. The video is bound by hash to the storyboard that made it, so publishing can refuse when they no longer match.
And one that only shows up later: at video twelve, the hard part is no longer making a video. It is knowing which of the twelve are still true.
- a fifteen-second silent clip you will watch once and throw away
- you have no ffmpeg, no Chrome, or no voice keys
- you want to hand-edit it afterwards in a real editor
- it isn't a web app: desktop, mobile and terminal are all out of scope
For a throwaway clip, write the script. For a video that goes somewhere public, shows real data, or will need re-recording as the product moves, the expensive part is the checking — and that is what this is.
Twenty-five projects can drive a browser and call a text-to-speech API. Here is what happens after the file exists.
A receipt binds the video to the storyboard that produced it.
{
"profile": "showcase",
"storyboard": { "sha256": "9f2c…", "bytes": 8412 },
"artifact": { "sha256": "b71a…", "durationMs": 172400 },
"summary": { "errors": 0, "warnings": 1, "checks": "21/21" },
"rehearsal": { "status": "agreed", "passes": 2 },
"frameEvidence": { "status": "passed", "frames": 10 },
"narrationCheck": { "verified": 10, "unverified": 0, "inconclusive": 0 },
"humanReview": { "status": "pending", "reviewer": null }
}Publishing refuses unless the profile is showcase, the errors are zero, and
both hashes still match the bytes on disk. Edit the storyboard after
recording and the publish is refused, because the chapter timestamps would no
longer match the video.
A failed run refuses, and says exactly what to do. This is a real refusal, from the bundled fixture recorded with the local silent voice at the publishable profile:
✗ audio_present
mean volume -91 dB
✓ privacy_clean
✓ never_show_clean
✓ egress_policy
✓ external_credential_free
✓ file_scope
✓ secret_scrub
28/31 showcase, 2 errors, 1 warnings
checks failed: nothing was delivered, and the previous artifact is untouched.
✗ audio/silent-track mean volume -91 dB is outside the usable band
at artifact=out/tour/.staging-RDYfQ6/tour.mp4
meanDb: -91
fix: check the narration clips mixed in
fix: re-run the mux
A locator failure reads the same way, and carries the frame it was looking at:
✗ step/locator-unresolved waiting for getByText('Apply filters')
at sceneId=filter stepIndex=2 do=click
screenshot: out/tour/evidence/fail-filter-2.png
url: http://localhost:8000/reports
visibleAlternatives: ["Apply filter","Clear filters","Filter by account"]
fix: use "text": "Apply filter"
fix: use "role": "button", "name": "Apply filter"
human_review stays pending until a person says they watched it. It is
never inferred and never claimed on their behalf. skipped means a tool was
unavailable, and never that a check passed.
Not a claim, a suite. Each fixture is a small server that reproduces a SHAPE, and the engine drives all of them with one storyboard vocabulary and no framework detection anywhere.
| Fixture | Auth | Readiness | Proves |
|---|---|---|---|
| static site | none |
full page load | no framework at all |
| server-rendered + CSRF form login | form-login |
one paint | the Django / Rails / Laravel shape |
| client-hydrated shell | none |
hydration settle | the SPA shape: content after a fetch |
| an app that never settles | none |
never | readiness/timeout fires instead of filming a spinner |
node test/run.mjs conformanceReadiness is measured, never waited out: readyState, then network quiet, then
no running animation, then optionally the app's own busy selector. There is no
if (isNextJs) anywhere in the engine, and a test greps for it.
A Rushes video alternates designed slides that explain a concept with the live application filmed proving it. Slides carry the idea; the app carries the evidence.
| A designed slide | The live app |
|---|---|
![]() |
![]() |
Slides are authored as JSON blocks whose connectors are measured after layout rather than predicted, or as free HTML when a slide has no topology. Both pass the same rendered-output checks: the safe area, the type floor, the contrast ratio, the embedded face, and every declared beat firing. Slide beats anchor to words in the narration, so re-recording with a different voice re-syncs every beat with no storyboard edit.
rushes slides preview renders every slide to a PNG with no voice, no recording
and no ffmpeg, seconds, and no spend. That is where a design is corrected, not
inside a finished video.
Nothing below is needed for the journey above. This is the escape hatch: what to run when you want to drive it yourself, or when something needs checking.
rushes demoRushes ships a small example application, a pretend accounting page. This starts
it on a free port, waits until it answers, films it, and stops it again. A real
MP4 lands in ./rushes-demo/out/tour/ in about two minutes. Worth running once
after installing, to prove your machine is set up before your own app is
involved.
rushes doctorPrints what is missing (Node, ffmpeg, a browser, the voice keys) and the one command that installs each. Run this first, always.
The agent runs these for you. You can run them too:
| Command | What it does |
|---|---|
rushes setup |
check what is needed and install what can be installed |
rushes demo [dir] |
record a video of the bundled example app; no configuration at all |
rushes doctor |
check node, ffmpeg, chrome, the browser engine and the optional keys |
rushes init |
probe the app and scaffold rushes.config.json |
rushes login |
sign in by hand once, headed, and save the browser state |
rushes discover <id> |
walk the app and draft a storyboard with expects pre-filled |
rushes validate <id> |
schema, lint, and a live dry run of every step and expect |
rushes rehearse <id> |
two silent passes; they must agree before you record |
rushes build <id> |
voice, record, compose and check into a staging directory |
rushes deliver <id> |
build, then commit atomically on pass and write the receipt |
rushes evidence <id> |
keyframes and the narration check, from the DELIVERED mp4 |
rushes recut <id> |
re-compose from the recording and the timeline; no re-record |
rushes rerun <id> |
re-record and report, per scene, what changed since the last delivery |
rushes score <id> |
an advisory score sheet: pacing, motion, reading rate. Not a gate |
rushes formats <id> |
a GIF, a vertical crop and editor stems from the same capture |
| `rushes slides <build | check |
rushes check <id> |
run the checker against what is already on disk |
rushes status |
the catalogue: what was built, published, and has since drifted |
rushes publish <id> |
optional; refuses without a passing receipt |
rushes publish-auth |
optional; one-time OAuth for the upload module |
rushes clean <id> |
sweep intermediates; deliverables are never touched |
rushes help [command] |
this list |
The three worth knowing: validate checks a storyboard against your live app and
costs nothing, rehearse runs it twice silently and refuses if the two passes
disagree, and deliver is the only one that spends anything.
Three lines is a valid config:
{ "baseUrl": "http://localhost:8000" }Then auth, in the order of how many people need each rung:
{ "auth": { "kind": "none" } } // a public site
{ "auth": { "kind": "storage-state" } } // most people
// then: rushes login , sign in by hand once, headed, however your app requires
{ "auth": { "kind": "form-login", "path": "/accounts/login/",
"fields": { "username": "${DEMO_USER}", "password": "${DEMO_PASS}" },
"csrfField": "csrfmiddlewaretoken" } }jwt-cookie, basic and header are there too. Everything else, readiness,
seeding, dismissers, branding, redaction, external navigation, publishing, is in
references/config-contract.md.
rushes init probes your app and scaffolds the file with what it can detect.
| Check | Measures | standard | showcase |
|---|---|---|---|
storyboard_schema |
schema + cross-field lint | error | error |
config_valid |
project config passes its schema and every ${VAR} resolves | error | error |
steps_resolved |
zero step/* errors in problems.json | warn | error |
scene_expects |
every scene expect satisfied | warn | error |
rehearsal_agreed |
the two rehearsal passes matched | ignore | error |
timeline_complete |
every scene present, monotonic, non-overlapping | error | error |
narration_covered |
every scene has audio; audio fits the scene | error | error |
dead_air |
per-scene gap at or under 8s | warn | error |
audio_present |
volumedetect mean between -30 and -6 dB | warn | error |
audio_loudness |
integrated loudness -18..-14 LUFS | ignore | warn |
video_stream |
resolution, fps and duration as configured | error | error |
no_black_scene |
scene midpoint keyframe not uniformly dark | ignore | error |
scene_opens_on_its_slide |
a slide scene is showing its slide when its narration starts | error | error |
captions_aligned |
cues exist; the last cue ends before the video does | warn | error |
caption_reading_rate |
no cue over ~20 characters per second | ignore | warn |
privacy_clean |
no secret or PII pattern in captured visible text | error | error |
never_show_clean |
no never-show term in narration, captions or evidence | error | error |
assert_metrics |
every assert satisfied against live data | error | error |
youtube_meta |
title within the limit; chapters 3+ or none; no orphan labels | warn | error |
narration_check |
zero unverified scenes from the vision check | ignore | error |
slide_font_embedded |
computed font-family is the embedded face on every slide | error | error |
slide_no_overflow |
no block whose scrollWidth exceeds its clientWidth | error | error |
slide_safe_area |
no element outside the frame | error | error |
slide_min_font |
no rendered text below the size floor | warn | error |
slide_contrast |
every text/background pair at or above 4.5:1 | warn | error |
slide_beats_fired |
every declared beat fired before the scene ended | warn | error |
slide_word_count |
words per slide under the ceiling | ignore | warn |
slide_edge_through_node |
no connector runs through a box it does not connect | error | error |
slide_route_composition |
no unrelated connectors cross or share a corridor | warn | error |
slide_label_clearance |
no connector label covers a box, a route or another label | ignore | warn |
slide_projected_readability |
the smallest text still readable once projected into a video player | ignore | warn |
slide_authored_fidelity |
authored slides stay on the project palette and face (L18) | ignore | warn |
slide_source_truth |
generated slide content matches the repo it was derived from | error | error |
slide_golden |
rendered slide matches its checked-in golden PNG | ignore | warn |
publish_consent |
the publish-safety question was answered yes and recorded | error | error |
recording_identity |
the recording identity is dedicated, not an operator account | error | error |
egress_policy |
every host contacted, and every redirect hop, passed the resolved-IP classification | error | error |
external_allowlisted |
every off-origin navigation named a host in external.allow | error | error |
external_credential_free |
no external request carried a header or basic credential | error | error |
file_scope |
every file:// target lay inside the compiled slide directory | error | error |
no_pending_restore |
no leftover .rushes/pending-restore.json | error | error |
receipt_auditable |
every auditable receipt field is present and non-empty | error | error |
secret_scrub |
no resolved secret value appears in any diagnostic, receipt or log | error | error |
env_allowlist |
the skill's .env carries only the two voice keys | error | error |
auth_effective |
the app is signed in after the auth strategy ran | error | error |
engine_neutral |
no product-specific identifier appears in lib/, bin/ or schemas/ | ignore | error |
This is a browser-driving tool that handles credentials and can publish publicly, so the constraints are stated out loud rather than assumed.
- Two environment variables, both for the voice. Rushes never stores another
system's secret. A build fails if its own
.envgrows a third key. - Auth is a browser state file you captured, or an env var you already export.
rushes loginopens a headed browser, you sign in, and the skill never sees the credential. The state file is written0600and can never enter the packaged payload. - Every host is classified on its resolved IP, not its name, with every redirect hop re-checked and the passing address pinned for the connection.
- Leaving your app's origin strips credentials, and
file://is confined to the compiled slide directory. Neither is configurable. - Every frame is scanned for key-shaped strings and for your own never-show list, and a hit is an error at every quality profile.
- A command in a config never auto-runs. It is printed with its working directory and the config's sha256, and approval is recorded against that hash, so any edit invalidates it.
- Publishing is absent unless configured, gated on
--confirmand on a receipt whose hashes still match the bytes on disk. - No telemetry. Nothing is reported anywhere, ever.
Full model: SECURITY.md.
Linux, macOS and Windows. Same pipeline, same checks, same output on all
three. Node 22.6 or newer is the only hard requirement, plus ffmpeg and a
browser — and rushes setup handles the browser and prints the one command for
ffmpeg on the package manager you actually have.
| Linux | macOS | Windows | |
|---|---|---|---|
| record, check, deliver | yes | yes | yes |
| arm64 browser build, from the engine | yes | yes (Apple Silicon) | not shipped |
| install ffmpeg with | apt dnf pacman zypper apk |
brew |
winget choco scoop |
| where the browser is found | PATH, then the engine's own | PATH, /Applications, then the engine's own |
PATH, Program Files, then the engine's own |
stopping an app started by runner |
process group | process group | taskkill /T on the process tree |
the saved login state (.rushes/state.json) |
mode 0600 |
mode 0600 |
the folder's ACL — see below |
Run rushes doctor first on any machine. Its first line names the platform, and
every row tells you what is missing and the exact command that fixes it.
- Nothing will ask for Screen Recording permission, and it should not. The browser is captured page-side, not off your display, so macOS never sees a screen capture. Your other windows, notifications and menu bar can never end up in the video.
- Chrome is found automatically in
/Applications, even though it is not on your PATH. If you have none,rushes setupfetches a browser into a cache in your home directory. - ffmpeg is the one thing you install yourself:
brew install ffmpeg. Homebrew writes into a prefix you own, so nosudois involved. - The "not enough memory" refusal counts memory macOS will actually hand back, not just the free-page number — which is a few hundred megabytes on a healthy Mac and would otherwise refuse to record on a machine with plenty to spare.
- PowerShell or Command Prompt both work. Nothing needs WSL, though WSL2 works too and behaves exactly like Linux.
- ffmpeg must be on your PATH.
winget install --id Gyan.FFmpeg -eis the usual route;chocoandscoopare recognised too. Open a new terminal afterwards so the PATH change is picked up, then re-runrushes doctor. - Chrome or Edge is found automatically under Program Files. Otherwise
rushes setupfetches one. runner.startis your command, not ours. Rushes starts it and stops its whole process tree, but the command string itself has to be one your shell understands — write it for Windows, or skip therunnerblock and start the app in another terminal yourself.- The saved login state cannot be locked down the way it is elsewhere.
Windows has no POSIX permission bits, so
.rushes/state.jsoninherits the folder's ACL instead of being0600. It is a bearer credential: anyone holding it is signed in as you. On a shared machine, restrict the.rushesfolder — and on every machine, add.rushes/to your.gitignore, which is whatrushes loginreminds you to do when it saves the file. - Environment variables use PowerShell syntax, and
rushes doctorprints them that way:$env:ELEVENLABS_API_KEY="sk_...".
Everything the tool assumes about an operating system lives in one file,
lib/platform.ts — finding a binary, running a shell command,
stopping a process tree, measuring available memory, turning a path into a URL.
node test/run.mjs portability reads every other source file and fails if one of
those decisions leaks out of that module, and the CI workflow runs the suite on
all three operating systems rather than on Linux alone.
- Browser only. Native and desktop applications are a different product.
- Needs ffmpeg and a Chrome or Chromium binary. Rushes detects and instructs; it never downloads a browser.
- On Windows,
runner.startis your shell command, not ours. Rushes starts it and stops its whole tree; whether the command itself is written forcmdor for a POSIX shell is up to you. - The voice costs real characters unless you use the local provider, which is silent and good only for timing.
- A storyboard is authored per demo. It does not write itself yet.
- The narration check needs a model with image input. Without one, every
scene is reported
inconclusive, which is reported, not hidden. - No free-form node-and-edge diagrams. Composed slides cover topologies with a known shape. If a diagram genuinely needs arbitrary placement and crossing edges, film the real application instead; it is more truthful than a drawing of it anyway.
| version | 1.0.10 |
| node | >=22.6.0 |
| license | MIT |
| dependencies | ajv@8.20.0, ajv-formats@3.0.1, playwright@1.62.1 |
| optional | googleapis@144.0.0 |
See CONTRIBUTING.md. The short version: the engine knows
nothing about the app it films, readiness is measured rather than waited out, and
a check fails closed unless it is on a named exception list. All three are
enforced by tests.
MIT licensed.


{ "publish": { "youtube": { "titlePrefix": "Acme - ", "privacy": "unlisted", "tags": ["Acme", "demo"] } } }