Skip to content
Back to skills

Gen Vhs Demo

ASecurity

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

  • 16 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 25, 2026
developmentgoshellbashgitapidocumentation

Works with

  • claude code
  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned September 25, 2026

npx -y skills add sgaunet/claude-plugins --skill gen-vhs-demo --agent claude-code

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.

Security grade badge for Gen Vhs Demo
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/sgaunet-gen-vhs-demo/badge)](https://www.skillsdirectory.com/skills/sgaunet-gen-vhs-demo)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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 `![demo](...)` 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

     ![demo](<gif path relative to README>)
     ```
     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`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.