Installs into .claude/skills of the current project.
Are you the author of Gen Vhs Demo?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/sgaunet-gen-vhs-demo)
---
name: gen-vhs-demo
description: Generate a terminal demo GIF with VHS - records a verified CLI session, wires the task runner, pins the tool, and embeds the GIF in the README
argument-hint: "[--output=<path>] [--theme=<name>] [--no-render] [--dry-run] [--force] [<what to demo>]"
user-invocable: true
disable-model-invocation: true
allowed-tools: Read, Write, Edit, Grep, Glob, Bash(vhs:*), Bash(git:*), Bash(test:*), Bash(mkdir:*), Bash(ls:*), Bash(command:*), Bash(task:*), Bash(make:*), Bash(mise:*), Bash(bash:*), Task, AskUserQuestion
---
# Generate VHS Demo
Produce an animated terminal demo of the current project's CLI using
**[VHS](https://github.com/charmbracelet/vhs)**: a `.tape` script, the rendered `.gif`, a task
runner entry to regenerate it, a pinned `vhs` version in `mise.toml`, and a `Demo` section in the
README that displays it.
## Why This Skill Exists
**Problem**: A CLI's README is a wall of text. A demo GIF is the single highest-value thing you
can add to it, but producing one means learning VHS tape syntax, guessing which commands make a
good 40-second story, wiring a regeneration task, pinning the tool, and editing the README - 20-30
minutes per repo. Worse, demo tapes rot silently: flags get deprecated, fixture paths move, and
nobody notices until the GIF shows a broken command to every visitor.
**Solution**: One invocation that:
- Discovers the CLI's real command surface from source, README, and integration tests
- **Executes every candidate command and requires exit 0** before it enters the tape, so the GIF
cannot show a broken invocation
- Rejects commands that depend on gitignored or untracked fixtures, so the demo reproduces from a
clean clone
- Writes the `.tape`, renders the `.gif`, and wires `task demo` / a `make demo` target
- Pins `vhs` in `mise.toml` so local and CI renders match
- Inserts the `` reference into the README at the right heading depth
## Content Generation Rules
**CRITICAL**: When generating any content (tape files, task entries, README sections, or any other
output):
- **NEVER** add Claude Code attribution or co-authorship
- **NEVER** include phrases like "Generated by Claude", "Created with Claude Code", or similar
- **NEVER** add "Co-Authored-By: Claude" in commit messages or documentation
- All generated content should appear as if written by a human developer
**Additionally, for recorded terminal content:**
- **NEVER** bake real credentials into a tape - no live API keys, tokens, private keys, internal
hostnames, or customer data. Use obviously-fake values (`demo@example.com`) or generate the
material inside a `Hide` block (`openssl rand -hex 32`).
- **NEVER** reference a fixture that is gitignored or untracked. The tape must render from a clean
clone.
- **NEVER** use a deprecated flag alias - deprecation warnings end up in the recording.
## Process (6 Phases)
### Phase 1: Prerequisites Validation
1. **Validate Git repository:**
```bash
git rev-parse --git-dir
```
- If fails: Exit with error "Not a git repository. Run 'git init' first."
2. **Resolve repository root:**
```bash
git rev-parse --show-toplevel
```
Store as `repoRoot`. All default paths resolve against it.
3. **Detect the VHS toolchain** (non-blocking - a tape is still worth writing without it):
```bash
command -v vhs && vhs --version
command -v ttyd
command -v ffmpeg
```
- Store `vhsInstalled`, `ttydInstalled`, `ffmpegInstalled`.
- Rendering requires **all three**. `vhs` alone is enough to write and `vhs validate` a tape.
- Store `canRender = vhsInstalled && ttydInstalled && ffmpegInstalled`.
4. **Verify skill assets:**
- Use Read to confirm `${CLAUDE_PLUGIN_ROOT}/assets/gen-vhs-demo/vhs-cheatsheet.md`
exists.
- If missing: Exit with error "Cheatsheet not found at
${CLAUDE_PLUGIN_ROOT}/assets/gen-vhs-demo/vhs-cheatsheet.md. Plugin may be corrupted.
Reinstall the software-engineering plugin."
### Phase 2: Argument Parsing
Parse `$ARGUMENTS` (or `$argument`, depending on host syntax). Tokenize on spaces, then partition
into flags and a free-text remainder.
**Supported flags:**
- `--output=<path>` - explicit path for the `.tape` file. The GIF path is the same path with
`.tape` replaced by `.gif`.
- `--theme=<name>` - VHS theme name. Validate against `vhs themes 2>&1` (it prints to **stderr**).
Invalid name: warn and omit `Set Theme` rather than writing a name that aborts the render.
- `--no-render` - write the tape and wiring, skip the `vhs` render step.
- `--dry-run` - print the tape and the planned edits; write nothing.
- `--force` - skip the confirmation gate.
**Everything else** is joined with spaces as `focus` - a free-text hint about what the demo should
show (e.g. "the encode and decode flow, plus JSON output"). Empty is fine; Phase 3 then picks the
scenes itself.
**Default output path** (when `--output` is absent), in order:
1. `<repoRoot>/doc/` if that directory exists
2. `<repoRoot>/docs/` if that directory exists
3. otherwise create `<repoRoot>/doc/`
Default filenames: `demo.tape` and `demo.gif`.
### Phase 3: Context Gathering
1. **Read the cheatsheet** to ground the tape in real VHS syntax:
```
Read: ${CLAUDE_PLUGIN_ROOT}/assets/gen-vhs-demo/vhs-cheatsheet.md
```
2. **Launch one Explore subagent** to map the CLI:
```
Task(
subagent_type: "Explore",
description: "CLI surface for demo",
prompt: "
Map this repository's CLI so a terminal demo can be scripted. Return JSON:
{
binaryName: string,
buildCommand: string, // how the binary is produced
buildOutputDir: string, // where it lands, relative to repo root
subcommands: [{name, purpose, requiredFlags, exampleInvocation}],
readmeExamples: [copy-pasteable command lines found in README],
testExamples: [command lines found in integration tests, with the literal
fixture values they use],
fixtureDependencies: [file paths any example reads],
buildSystem: 'taskfile' | 'makefile' | 'none',
buildTaskName: string | null, // e.g. 'binary' - the task that builds the CLI
hasMiseToml: boolean,
readmeTopHeadingLevel: 1 | 2, // '# Title' vs '## Title' convention
readmeDemoSectionExists: boolean
}
Detection strategy:
- Entry point: main.go, cmd/root.go, src/main.rs, bin/*, package.json 'bin'
- Subcommands: cobra/clap/click/commander registrations, or the --help text
- Integration tests are the best source of literal working arguments
- Note DEPRECATED flag aliases explicitly so they can be avoided
Return concise JSON, no prose.
"
)
```
3. **Filter out unreproducible examples.** For every path in `fixtureDependencies`:
```bash
git ls-files --error-unmatch <path>
```
A non-zero exit means the fixture is untracked or gitignored. Drop any example that depends on
it, or replace it with a scene that generates the fixture inside a `Hide` block.
4. **Draft 3-5 scenes.** A scene is one narration comment plus one or two commands. Structure the
sequence as a story: the primary use case first, then a variation, then an error case or a
power-user flag. Honour `focus` when it was supplied. Keep the whole tape under ~45 seconds.
5. **Verify every candidate command.** This is the step that keeps the GIF honest:
```bash
# build first
<buildCommand>
# then run each candidate, from repoRoot, with the binary on PATH
bash -c 'export PATH="<repoRoot>/<buildOutputDir>:$PATH"; <candidate command>'
```
- Exit 0 and sensible output: keep the command.
- Non-zero, or stderr contains "deprecated": repair it (correct flag, correct fixture) and
re-run, or drop the scene.
- Error-case scenes are exempt from the exit-0 rule, but must fail *the intended way* - confirm
the error message is the one the scene claims to show.
- Never write an unverified command into the tape.
### Phase 4: Confirmation Preview
Build the preview message:
```
π¬ VHS Demo Preview
Tape: <relative tape path>
GIF: <relative gif path>
Theme: <theme or "(VHS default)">
Render: <"yes" if canRender && !--no-render else "no (tape only)">
Scenes (~<N>s total):
1. <narration> -> <command>
2. ...
Files to change:
β’ <tape path> <create | overwrite>
β’ <Taskfile.yml | Makefile> add '<demo>' <task | target>
β’ mise.toml add vhs = "<version>" <or "already pinned">
β’ README.md add Demo section <or "already present">
<CONDITIONAL WARNINGS>
```
**Conditional warnings:**
If `canRender = false`:
```
βΉοΈ VHS toolchain incomplete - tape will be written, rendering skipped
Missing: <list of vhs / ttyd / ffmpeg>
Install:
β’ mise: mise use vhs@0.11.0
β’ macOS: brew install vhs ttyd ffmpeg
β’ Verify: vhs --version && command -v ttyd && command -v ffmpeg
```
If the tape or GIF already exists:
```
β οΈ WARNING: <path> already exists
This will OVERWRITE the current demo.
Consider committing the existing version first.
```
If `buildSystem = 'none'`:
```
βΉοΈ No Taskfile.yml or Makefile found
The regeneration command will be reported instead of wired up.
```
**Confirmation logic:**
If `--dry-run`: print `[DRY RUN MODE - No files will be written]`, jump to Phase 6, print the tape
and the planned edits, write nothing.
If `--force`: print `[--force mode enabled, skipping confirmation]` and proceed to Phase 5.
Otherwise use AskUserQuestion:
```
Question: "Create the demo at <tape path> and update the task runner, mise.toml, and README?"
Options:
- "Yes, create the demo" (proceed to Phase 5)
- "Tape only, no other edits" (Phase 5 step 1 only)
- "No, cancel" (exit with "Operation cancelled by user")
```
### Phase 5: Generation
Every edit is **idempotent** - detect the existing state first and skip anything already correct.
1. **Write the tape.** Follow the cheatsheet. Shape:
```tape
# <Project> demo - regenerate with: <task demo | make demo | vhs <path>>
Output <gif path relative to repo root>
Set Shell "bash"
Set FontSize 18
Set Width 1200
Set Height 700
Set Padding 20
Set Theme "<theme>" # omit when no valid theme was resolved
Hide
Type "export PATH=$PWD/<buildOutputDir>:$PATH" Enter
Type "export PS1='$ '" Enter
Type "clear" Enter
Show
Type "# <narration for scene 1>" Enter
Sleep 500ms
Type "<verified command>" Enter
Sleep 2s
...
```
- `Output` is relative to the directory `vhs` runs from (the repo root), **not** to the tape.
- Prefer the `Hide` PATH preamble over `Require <binary>`; a repo-built binary is not on
`$PATH`.
- Narrate every scene with a `# ...` comment line - a silent GIF needs captions.
- Hold output on screen with `Sleep 1500ms`-`Sleep 2s` after each command; use `Sleep 500ms`
between typed lines.
- Create the parent directory first: `mkdir -p "$(dirname <tape path>)"`.
2. **Wire the task runner:**
- `Taskfile.yml` exists β add, preserving the file's existing indentation and quoting style:
```yaml
demo:
desc: "Generate demo.gif"
deps: [<buildTaskName>]
cmds:
- vhs <tape path>
```
Omit `deps` when no build task exists. If a `demo` task is already present, update its `cmds`
and `deps` rather than adding a duplicate.
- No Taskfile but a `Makefile` exists β add a `.PHONY` `demo` target that depends on the build
target and runs `vhs <tape path>`.
- Neither β skip, and report the raw `vhs <tape path>` command in Phase 6.
3. **Pin the tool in `mise.toml`:**
- File exists with a `[tools]` table β append `vhs = "0.11.0"` if absent. **Only add**; never
reorder or rewrite other entries (`/gen-taskfiles` owns this file canonically).
- Already pinned at any version β leave it alone and note the version in the report.
- File absent β use AskUserQuestion to offer creating a minimal one:
```toml
[tools]
vhs = "0.11.0"
```
Decline β skip and note it.
4. **Update the README:**
- `readmeDemoSectionExists` β verify the image path matches the GIF actually produced, correct
it if it drifted, otherwise leave the section untouched.
- Absent β insert a section using the file's own top heading depth (`#` or `##` per
`readmeTopHeadingLevel`):
```markdown
# Demo

```
Place it after the intro/description and badges, before Install or Usage - high enough that a
visitor sees it without scrolling past the whole feature list.
### Phase 6: Render & Success Report
1. **Validate the tape** (works even without `ttyd`/`ffmpeg`):
```bash
vhs validate <tape path>
```
Non-zero exit means a syntax error - fix the tape before rendering. VHS points at the offending
line.
2. **Render** when `canRender && !--no-render && !--dry-run`, from the repo root:
```bash
vhs <tape path>
```
Rendering takes roughly the tape's wall-clock duration plus encoding. Treat a non-zero exit as a
**soft** failure - the tape and wiring were still produced.
3. **Verify the artifact:**
```bash
test -f <gif path> && ls -lh <gif path>
```
**If `--dry-run`:** report that nothing was written, print the full tape content, list the edits
that would have been made, and note that dropping `--dry-run` performs them.
**Otherwise report** - cover these facts; no fixed layout required:
- **Files written:** the tape (with its line count), the GIF (with its size on disk), and each
wiring file that changed. Name the ones deliberately skipped and why.
- **Scenes recorded:** the narration lines, so the user can see the story without opening the GIF.
- **Verification performed:** state plainly that every command in the tape was executed and exited
0 (or, for error scenes, produced the intended error). If any candidate was dropped, say which
and why.
- **Next steps:** view the GIF (`open <gif>` on macOS, `xdg-open <gif>` on Linux); edit the tape
and re-render with `task demo` / `make demo` / `vhs <tape>`; commit both the tape and the GIF so
the demo stays reproducible.
- **File weight:** if the GIF exceeds ~5 MB, say so and suggest, in order: shorten `Sleep`
durations, lower `Set Framerate` to 24, then reduce `Set Width`/`Set Height`.
- **Missing tools:** if `canRender = false`, name exactly which of `vhs`/`ttyd`/`ffmpeg` is missing
and give the `mise use vhs@0.11.0` / `brew install` line, then the command to render once
installed.
- **Render failure:** if `vhs` exited non-zero, quote its stderr and note that the tape was written
successfully, so a manual `vhs <tape path>` is all that is needed after fixing the cause.
## Integration with Other Skills
- **Before `/gen-vhs-demo`**: `/gen-taskfiles` (go-specialist) creates the `Taskfile.yml` and
`mise.toml` this skill extends. Running it first produces cleaner wiring.
- **After `/gen-vhs-demo`**:
- `/gen-claude` picks up the new `demo` task when regenerating CLAUDE.md.
- `/upd-project-description` benefits from a README that now leads with a demo.
- Re-run this skill after any CLI flag change - the verification step in Phase 3 is what
catches a tape that has rotted.
## Examples
```bash
# Auto-discover the CLI and record a demo
/gen-vhs-demo
# Focus the demo on a specific flow
/gen-vhs-demo the encode and decode round trip, plus JSON output
# Custom path and theme
/gen-vhs-demo --output=docs/assets/cli.tape --theme="Catppuccin Mocha"
# Preview the tape without writing anything
/gen-vhs-demo --dry-run
# Write the tape and wiring, render later (e.g. ttyd not installed)
/gen-vhs-demo --no-render
# Regenerate non-interactively
/gen-vhs-demo --force
```
## Limitations & Caveats
- **Rendering needs three binaries**: `vhs`, `ttyd`, and `ffmpeg`. Without all three only the tape
is produced.
- **Fonts must exist locally**: `Set FontFamily` silently falls back when the font is not
installed, so renders differ across machines unless the font is guaranteed. This skill omits
`Set FontFamily` by default.
- **Interactive TUIs are hard to script**: VHS drives keystrokes, not intent. A CLI with prompts,
spinners, or alternate-screen output may need hand-tuned `Sleep`/`Wait` directives.
- **GIFs are heavy**: a 45-second 1200x700 recording lands around 2-6 MB. Commit it anyway - it is
the README's most valuable asset - but keep an eye on the size.
- **The verification step runs your CLI for real**: commands that mutate state, hit the network, or
cost money are unsuitable for a demo. Prefer read-only or self-contained flows.
- **Timing is not deterministic**: a command slower than its `Sleep` gets its output cut off. Use
`Wait` for anything with variable duration.
## Error Recovery
**If `vhs validate` fails:**
1. Read the reported line - VHS names the offending keyword.
2. Confirm the directive exists in the cheatsheet; VHS rejects unknown keywords outright.
3. Check that `Output` and all `Set` directives precede the first `Type`.
**If the render produces a GIF with "command not found":**
1. The binary was not on `$PATH`. Confirm the build ran and that the `Hide` preamble points at the
real build output directory.
2. Add a build dependency to the `demo` task so the binary always exists before `vhs` runs.
**If output is cut off mid-command:**
1. Increase the `Sleep` after that command, or
2. Replace it with `Wait` (optionally `Wait+Line /<regex>/`) followed by a short `Sleep`.
**If the GIF is too large:**
1. Trim `Sleep` durations - usually 40% of the runtime.
2. `Set Framerate 24`.
3. Reduce `Set Width`/`Set Height`; keep the aspect ratio.
**If `ttyd` or `ffmpeg` is missing:**
```bash
mise use vhs@0.11.0 # pins vhs only
brew install ttyd ffmpeg # macOS
```
Then re-render with `task demo` (or `vhs <tape path>`).
## Additional Resources
- VHS repository: https://github.com/charmbracelet/vhs
- Command reference: https://github.com/charmbracelet/vhs#vhs-command-reference
- Theme list: `vhs themes 2>&1` (prints to stderr)
- Scaffold a starter tape: `vhs new demo.tape`
- Record a live session into a tape: `vhs record > demo.tape`
- Local cheatsheet: `${CLAUDE_PLUGIN_ROOT}/assets/gen-vhs-demo/vhs-cheatsheet.md`