A CLI toolkit and Claude Code skill for creating narrated demo videos of web applications. You describe what to show, Claude Code drives the browser, and the toolkit renders a polished mp4 with voiceover.
Claude Code is the agent. The toolkit provides browser management, page inspection, segment playback, and video rendering as CLI commands. The skill file (SKILL.md) teaches Claude Code how to use them.
- You write a playbook (YAML) that lists the segments of your demo — each with narration text and a description of what should happen on screen.
- Claude Code opens a real browser, reads the page's accessibility tree, and fills in the concrete actions (clicks, typing, waits) for each segment.
- You review each segment live in the browser, iterate with Claude Code until it looks right, then render the final video with TTS narration.
You: "Create a demo showing the new dashboard filters"
→ Claude Code writes the playbook, opens the browser,
authors actions by inspecting the page, tests each
segment, and renders the final mp4.
- Node.js 20+
- ffmpeg with libx264 and aac encoders (ffprobe ships with it)
- OPENAI_API_KEY environment variable (for TTS narration)
- Claude Code with skills support
Add this repo as a skill in your project's .claude/settings.json:
{
"skills": [
"https://github.com/splitbrain/ndemo"
]
}Clone directly into a Claude Code skills directory:
# Project-level (this project only)
git clone https://github.com/splitbrain/ndemo .claude/skills/ndemo
# Personal (available in all projects)
git clone https://github.com/splitbrain/ndemo ~/.claude/skills/ndemoThis is useful during development — you can edit the skill files and the CLI source directly, and Claude Code picks up changes immediately.
You can also symlink an existing clone:
ln -s /path/to/your/ndemo ~/.claude/skills/ndemoEither way, the skill file tells Claude Code how to build the toolkit and install Playwright on first use. Everything runs from within the skill's own directory — nothing gets copied into your project.
In Claude Code, just say:
Create a narrated demo of my app at http://localhost:3000
Claude Code will:
- Build the toolkit (first time only)
- Create a playbook YAML in your project
- Open the browser and navigate to your app
- Inspect the page and author actions for each segment
- Test each segment live
- Render the final mp4 with TTS narration
Be specific about what the demo should show, what state needs restoring, and how to authenticate:
Create a narrated demo for editing a wiki page. The app runs at http://localhost:8080/wiki. The demo should: (1) show the start page, (2) click edit, (3) type some text, (4) save the page. The demo modifies data/pages/start.txt so save a copy for restoration. Login with admin/admin if needed.
Claude Code will create a playbook directory with this structure:
demo/
edit-page/
edit-page.yaml ← playbook
fixtures/ ← copies of files to restore during setup
audio/ ← TTS files (generated)
video-raw/ ← raw recording (generated)
demo.mp4 ← final output (generated)
The fixtures/ directory holds copies of files that the demo modifies.
Setup steps copy them back before each run so the demo is always
repeatable.
You can also create the playbook yourself and ask Claude Code to fill in the actions:
# demo/edit-page/edit-page.yaml
app:
url: http://localhost:8080/wiki
setup:
- run: cp demo/edit-page/fixtures/start.txt data/pages/start.txt
- type: click
target: { role: link, name: "Login" }
if:
hidden: ".user-info"
segments:
- id: intro
narration: "Welcome to our wiki. Let's edit a page."
intent: "show the start page"
actions:
- type: wait
duration: 2000
- id: open-editor
narration: "Click the edit button to open the editor."
intent: "click the edit button"
actions: []Fill in the actions for my demo playbook at demo/edit-page/edit-page.yaml
The skill file teaches Claude Code to run these commands automatically, but you can also run them directly:
<skill-directory>/ndemo <command>| Command | Description |
|---|---|
ndemo open <playbook> |
Launch a headed browser daemon and navigate to the app |
ndemo close |
Shut down the browser daemon |
ndemo reset |
Navigate back to the app URL with a fresh state |
ndemo page-state |
Print the current page's accessibility tree |
ndemo page-state --screenshot |
Same, plus save a screenshot |
ndemo play <playbook> |
Play all segments in the live browser |
ndemo play <playbook> --segment <id> |
Play just one segment (rewinds first) |
ndemo play <playbook> --from <id> |
Play from a segment to the end |
ndemo play <playbook> --from <id> --to <id> |
Play a range of segments |
ndemo play <playbook> --audio |
Play with TTS narration (combinable with other flags) |
ndemo render <playbook> |
Full pipeline: TTS, headless replay, merge to mp4 |
ndemo render <playbook> --output path.mp4 |
Render to a specific output path |
ndemo doctor |
Check that all dependencies are installed |
app:
url: https://myapp.dev # required
viewport: # optional, defaults shown
width: 1920
height: 1080
scale: 2 # device scale factor
zoom: 1.25 # CSS zoom
colorScheme: light # light or dark
setup: # optional steps to run on load
- run: cp fixtures/page.txt data/ # shell commands for file ops
- type: click # browser actions
target: { role: button, name: "Login" }
if: # conditional (skip if not met)
visible: ".login-form"
titleCard: # optional, adds a title frame
title: "My Demo"
subtitle: "Optional subtitle" # optional
duration: 3000 # milliseconds (default 3000)
tts: # optional, defaults shown
provider: openai
voice: alloy
speed: 1.0
recording: # optional, defaults shown
outputDir: . # relative to playbook directory
fps: 30
segments:
- id: segment-name # lowercase, hyphens, unique
narration: "What the viewer hears."
intent: "What happens on screen (for Claude Code's reference)."
timing: after # after (default) or parallel
actions:
- type: click
target: { role: button, name: "Settings" }
done:
visible: ".settings-panel"
- type: wait
duration: 2000| Type | Required fields | Notes |
|---|---|---|
click |
target |
|
type |
target, text |
delay: 60-100 for human-like typing |
hover |
target |
|
scroll |
target |
Scrolls the element into view |
wait |
duration (ms) |
Pause so the viewer can see what happened |
press |
key |
Keyboard key, e.g. Enter, Escape |
select |
target, option |
Dropdown selection |
Targets tell Playwright how to find an element. Use the output of ndemo page-state to pick the right one:
target: { role: button, name: "Settings" } # accessibility role + name
target: { label: "Email address" } # form label
target: { placeholder: "Search..." } # input placeholder
target: { text: "Learn more" } # visible text
target: { testId: "submit-btn" } # data-testid attribute
target: { selector: "#my-element" } # CSS selector (last resort)Every action that changes the page should have a done condition so the next action waits for the page to be ready:
done:
visible: ".panel" # element appears
hidden: ".spinner" # element disappears
networkIdle: true # no pending network requests
stable: 500 # DOM unchanged for 500ms
url: "**/settings" # URL matches pattern
text: # element contains text
selector: ".status"
has: "Saved"
attribute: # element has attribute value
selector: html
name: data-theme
value: darkClaude Code (the agent)
├── reads SKILL.md (skill file) for workflow
├── reads the web app's source for context
├── edits playbook YAML
└── runs ndemo CLI commands
│
├── open ──── launches browser daemon
├── page-state ── reads accessibility tree
├── play ──── executes segments in live browser
├── render ── TTS + headless replay + merge
└── close ─── kills browser daemon
The toolkit deliberately avoids building its own agent loop, conversation manager, retry logic, or element discovery. Claude Code already does all of that — the skill file just teaches it the workflow.
MIT