RepoPilotOpen in app →

charmbracelet/vhs

Your CLI home video recorder 📼

Healthy

Healthy across the board

Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 5d ago
  • 16 active contributors
  • Distributed ownership (top contributor 48% of recent commits)
Show 3 more →
  • MIT licensed
  • CI configured
  • Tests present

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/charmbracelet/vhs)](https://repopilot.app/r/charmbracelet/vhs)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/charmbracelet/vhs on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: charmbracelet/vhs

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/charmbracelet/vhs shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across the board

  • Last commit 5d ago
  • 16 active contributors
  • Distributed ownership (top contributor 48% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live charmbracelet/vhs repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/charmbracelet/vhs.

What it runs against: a local clone of charmbracelet/vhs — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in charmbracelet/vhs | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch main exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 35 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>charmbracelet/vhs</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of charmbracelet/vhs. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/charmbracelet/vhs.git
#   cd vhs
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of charmbracelet/vhs and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "charmbracelet/vhs(\\.git)?\\b" \\
  && ok "origin remote is charmbracelet/vhs" \\
  || miss "origin remote is not charmbracelet/vhs (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
  && ok "default branch main exists" \\
  || miss "default branch main no longer exists"

# 4. Critical files exist
test -f "command.go" \\
  && ok "command.go" \\
  || miss "missing critical file: command.go"
test -f "evaluator.go" \\
  && ok "evaluator.go" \\
  || miss "missing critical file: evaluator.go"
test -f "draw.go" \\
  && ok "draw.go" \\
  || miss "missing critical file: draw.go"
test -f "main.go" \\
  && ok "main.go" \\
  || miss "missing critical file: main.go"
test -f "Makefile" \\
  && ok "Makefile" \\
  || miss "missing critical file: Makefile"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 35 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~5d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/charmbracelet/vhs"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

VHS is a CLI tool that records terminal sessions as GIFs or MP4s by parsing .tape files—a domain-specific language for scripting terminal interactions. It uses ttyd (terminal server) and ffmpeg (video encoding) as dependencies to capture and render terminal output, enabling developers to create reproducible terminal demos and integration test recordings without manual screen recording. Monolithic CLI binary structure: core command parsing in command.go, evaluation engine in evaluator.go, drawing/rendering in draw.go, error handling in error.go. Uses Cobra (spf13/cobra v1.10.2) for CLI framework. Entry point flows through tape file parsing → AST evaluation → ttyd subprocess spawning → ffmpeg video capture. Examples/ directory mirrors real-world usage patterns (bubbletea integration examples).

👥Who it's for

CLI tool developers and technical educators who need to generate terminal GIFs/videos for documentation, demos, and integration tests. Users write .tape files (similar to Elixir syntax) defining terminal commands, sleeps, and interactions, then VHS renders them to video—no manual recording or video editing required.

🌱Maturity & risk

Production-ready and actively maintained. The project uses Go 1.25.8, has comprehensive CI/CD via GitHub Actions (build.yml, lint.yml, goreleaser.yml, nightly.yml), includes Dockerfile for containerized execution, and maintains 172KB of Go code. Recent commits visible in dependabot sync and goreleaser workflows indicate ongoing maintenance. Well-documented with examples/ directory containing 20+ real .tape files (bubbletea examples).

Low risk overall. Dependency footprint is moderate (~25 explicit Go dependencies from charmbracelet ecosystem plus standard tooling). Critical runtime dependencies are external (ttyd, ffmpeg) which must be pre-installed—failure here is user-environment issue, not codebase. Single-point-of-failure on ttyd availability, but this is documented clearly. No evidence of abandoned issues or stale PRs visible in workflow configuration.

Active areas of work

Active maintenance with recent dependency updates (charmbracelet/lipgloss, charmbracelet/ssh bumped to latest 2025 versions). Nightly workflow suggests ongoing stability testing. Dependabot automation enabled (.github/dependabot.yml). No breaking changes evident in version constraints—supporting Go 1.25.8 as current minimum.

🚀Get running

git clone https://github.com/charmbracelet/vhs
cd vhs
make build
# or: go build -o vhs ./cmd/vhs
# Requires: ttyd and ffmpeg on PATH
brew install ttyd ffmpeg  # macOS
./vhs examples/bubbletea/simple.tape

Daily commands:

make build          # Compiles vhs binary
make test           # Runs Go tests (command_test.go present)
make lint            # Runs golangci-lint (.golangci.yml configured)
vhs <filename>.tape # Executes a tape file and generates output (GIF/MP4 per Output directive)

🗺️Map of the codebase

  • command.go — Core command execution engine that processes .tape script directives and orchestrates the recording workflow—every feature depends on this
  • evaluator.go — Script parser and evaluator that transforms .tape files into executable commands—essential for understanding the DSL and execution model
  • draw.go — Frame rendering and animation logic that converts terminal state into GIF/WebM output—critical path for all video generation
  • main.go — CLI entry point and command router using Cobra—defines the public API and top-level architecture
  • Makefile — Build and installation targets—contributors must understand the project's build system and dependencies
  • .goreleaser.yml — Release configuration for multi-platform binary distribution—essential for understanding the release pipeline
  • go.mod — Go module dependencies including creack/pty, go-rod, and charmbracelet ecosystem—defines the runtime stack

🧩Components & responsibilities

  • Evaluator (evaluator.go) (Go parser; lexer/tokenizer) — Parses .tape script syntax into an AST; validates commands and arguments before execution
    • Failure mode: Syntax errors reported to user; invalid script halts recording with clear error message
  • Command Executor (command.go) (creack/pty, file I/O, timing control) — Orchestrates the recording session: initializes PTY, routes commands to handlers, manages state
    • Failure mode: PTY creation fails → recording aborts; command execution timeout → script halts with error
  • Renderer (draw.go) (Image encoding, theme engine, lipgloss styling) — Converts terminal frames into images; applies themes, timing, and decorations; encodes GIF/WebM
    • Failure mode: Codec unavailable → fallback format or error; memory exhaustion on large frames → OOM
  • PTY Manager (implicit in command.go) (creack/pty, syscalls, POSIX terminal control) — Creates and manages pseudo-terminals; captures I/O; forwards signals
    • Failure mode: PTY unavailable on platform → fatal error; shell session crash → I/O read error

🔀Data flow

  • User input (.tape file)Evaluator — Script text → parsed AST (commands, arguments, timing)
  • EvaluatorCommand Executor — Validated command list → sequential execution loop
  • Command ExecutorPTY — Input actions (type, key press) → shell stdin; environment variables
  • PTYCommand Executor — Shell stdout/stderr → frame buffer; exit codes
  • Command ExecutorRenderer — Frame buffer snapshots at intervals → image encoding queue
  • RendererOutput file — Encoded frames + timing → GIF/WebM file on disk

🛠️How to make changes

Add a new .tape DSL command

  1. Define the command parsing rule in evaluator.go by adding a new case in the statement parsing switch (evaluator.go)
  2. Implement the command execution logic in command.go or a new handler file (command.go)
  3. Add tests for parsing and execution in command_test.go (command_test.go)
  4. Create an example .tape file in examples/ demonstrating the new command (examples/commands/mycommand.tape)

Add support for a new output format

  1. Extend the output format detection and routing in command.go (command.go)
  2. Implement format-specific encoding logic in draw.go (or create draw_format.go) (draw.go)
  3. Add integration tests for the new format in command_test.go (command_test.go)

Add a new built-in theme

  1. Create theme definition (JSON/YAML) in the embedded resources referenced by embed.go (embed.go)
  2. Register the theme in command.go's theme loading logic (command.go)
  3. Document the theme in THEMES.md with preview examples (THEMES.md)

🔧Why these technologies

  • Go + Cobra CLI framework — Fast, compiled binaries with rich CLI UX; Cobra provides standard command structure and help generation
  • creack/pty — Cross-platform pseudo-terminal handling for capturing interactive terminal sessions in real-time
  • go-rod (Chromium automation) — Enables recording of web-based TUIs and browser interactions within terminal sessions
  • Charmbracelet ecosystem (lipgloss, glamour, ssh, wish) — Native terminal styling, SSH server support, and rich text rendering for decorations and overlays
  • Custom .tape DSL — Declarative, human-readable script format for reproducible terminal recording sessions

⚖️Trade-offs already made

  • Synchronous script execution in Go rather than async/event-driven

    • Why: Simpler mental model for script authors; deterministic timing control; easier to debug
    • Consequence: Cannot record truly concurrent terminal operations; timing is sequential and script-driven
  • Embedded themes and presets rather than external configuration

    • Why: Self-contained binary; no external dependencies at runtime; consistent theming
    • Consequence: Updating themes requires rebuilding; less flexible for custom styling at install time
  • Direct PTY capture + GIF/WebM rendering vs. terminal recording library

    • Why: Full control over frame timing, decorations, and output quality; independence from third-party libraries
    • Consequence: Higher maintenance burden for cross-platform PTY handling; must manage codec versions externally

🚫Non-goals (don't propose these)

  • Real-time streaming or live video output (only file-based recording)
  • Support for graphical (non-terminal) applications or X11/Wayland window recording
  • Audio/sound capture (video-only, though theme decorations can simulate typing sounds visually)
  • Windows PowerShell or non-POSIX shells natively (Linux/macOS primary targets)

⚠️Anti-patterns to avoid

  • Tight coupling between script evaluation and command execution (Medium)evaluator.go + command.go boundary: Commands are parsed directly into imperative calls rather than a decoupled command queue; changes to command semantics require changes in both files
  • Global state for theme and styling configurationcommand.go (implicit theme globals): Theme and decoration settings appear to be globally scoped

🪤Traps & gotchas

ttyd and ffmpeg must exist on PATH at runtime—VHS won't start without them, and error messages may be opaque if they're missing. Font rendering depends on system fonts—Set FontSize in tapes requires fonts installed locally (no fallback). pty/terminal size coupling—Set Width/Height must match the terminal capabilities of the ttyd server, mismatches cause silent layout issues. subprocess signal handling—Tape execution doesn't gracefully handle SIGINT mid-execution (Enter key stops cleanly, Ctrl-C may leave ttyd zombie processes). No headless browser detection—go-rod/rod dependency suggests screenshot features may require display server (X11/Wayland) on Linux.

🏗️Architecture

💡Concepts to learn

  • Pseudo-terminal (PTY) — VHS uses creack/pty (v1.1.24) to spawn interactive terminal sessions; understanding master/slave PTY pairs is essential for debugging tape execution and signal handling
  • Domain-Specific Language (DSL) — The .tape file format is a custom DSL parsed by command.go; learning how command structs map to tape syntax helps contributors add new commands and features
  • Subprocess & Signal Handling — VHS spawns ttyd and ffmpeg as child processes (evaluator.go); understanding process lifecycle, pipes, and signal forwarding (SIGTERM/SIGINT) is critical for reliability
  • Frame capture & video encoding — draw.go captures terminal frames and pipes them to ffmpeg; understanding container formats (MP4, GIF), codecs (H.264), and fps/bitrate tradeoffs helps optimize output quality
  • Levenshtein distance — Dependency agnivade/levenshtein (v1.2.1) used for fuzzy command matching; VHS may autocorrect tape syntax errors—understanding edit distance helps debug parser behavior
  • Web scraping via browser automation — go-rod/rod (v0.116.2) dependency suggests VHS can capture browser output as terminal recording; useful for documenting web CLI tools like TUIs running in headless Chromium
  • Environment variable configuration — caarlos0/env (v11.4.0) enables VHS configuration via env vars (e.g., VHS_OUTPUT, VHS_WIDTH); understanding 12-factor app principles helps with containerized usage
  • charmbracelet/bubbletea — TUI framework widely used in VHS examples (examples/bubbletea/ contains 20+ reference implementations); understanding Bubbletea helps understand what VHS records
  • charmbracelet/wish — SSH server library used by VHS internals (dependency: github.com/charmbracelet/wish v1.4.7) for remote terminal handling and authentication
  • tsl0922/ttyd — Critical runtime dependency—VHS spawns ttyd as subprocess to run terminal sessions; understanding ttyd arguments and protocol helps debug recording issues
  • asciinema/asciinema — Alternative terminal recording format (.cast JSON files) vs VHS's tape DSL; VHS is more declarative/scriptable while asciinema captures live sessions
  • charmbracelet/glamour — Markdown renderer used by VHS (dependency: github.com/charmbracelet/glamour v1.0.0) for rendering formatted text output in tape examples

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive unit tests for evaluator.go

evaluator.go is a core component that processes VHS tape scripts, but there's no corresponding evaluator_test.go file despite command.go having command_test.go. This is critical for a tool that executes user-provided scripts. Tests should cover script parsing, command execution flow, variable handling, and error cases to ensure reliability.

  • [ ] Create evaluator_test.go with test cases for script parsing and evaluation
  • [ ] Add tests for each command type (Type, Sleep, Run, Capture, etc.) based on patterns in command_test.go
  • [ ] Test error handling for malformed scripts and edge cases
  • [ ] Test variable interpolation and environment handling
  • [ ] Ensure test coverage for the critical evaluation path that processes .tape files

Add unit tests for draw.go rendering logic

draw.go handles terminal rendering and frame composition, which is visually critical but currently has no corresponding draw_test.go file. This module needs tests to prevent regressions in GIF output quality, color rendering, and frame management.

  • [ ] Create draw_test.go with test cases for frame rendering
  • [ ] Add tests for ANSI color code handling and terminal escape sequences
  • [ ] Test frame composition and pixel/cell buffer operations
  • [ ] Include tests for different terminal width/height scenarios
  • [ ] Add tests for theme application and color palette rendering

Create GitHub Actions workflow for integration tests with .tape examples

The repo has many examples in examples/bubbletea/ and examples/cli-ui/ with .tape files, but there's no CI workflow specifically testing these example scripts execute successfully. This prevents regressions where script changes break real-world examples. Current workflows (build.yml, lint.yml) don't validate example tape files.

  • [ ] Create .github/workflows/examples-integration.yml workflow
  • [ ] Add steps to run vhs on sample .tape files from examples/ directory
  • [ ] Validate generated GIFs are created without errors
  • [ ] Test at least one example from examples/bubbletea/ and examples/cli-ui/
  • [ ] Configure workflow to fail if any example script execution returns non-zero exit code
  • [ ] Add caching for browser dependencies (go-rod uses Chromium)

🌿Good first issues

  • Add timeout parameter to Sleep command: Currently Sleep 5s is absolute; contributors could add Sleep 5s timeout to enforce maximum wait and fail gracefully if terminal command hangs—implement in command.go parser and evaluator.go execution path
  • Expand command_test.go coverage for edge cases: Test parsing of malformed tape files (missing Output, duplicate Set directives, invalid durations like 'Sleep abc')—currently only basic happy path tested
  • Document tape file schema as JSON Schema or XSD: Generate from command.go structs and publish to docs/schema.json for IDE autocomplete support—automate via build script in Makefile

Top contributors

Click to expand

📝Recent commits

Click to expand
  • e9c41d3 — chore: remove CODEOWNERS (andreynering)
  • 6ec7329 — chore(deps): bump the all group with 2 updates (#745) (dependabot[bot])
  • 6f0c046 — ci: sync golangci-lint config (#723) (github-actions[bot])
  • 67e9d17 — chore(deps): bump the all group with 2 updates (#725) (dependabot[bot])
  • c6af91a — chore(deps): bump the all group across 1 directory with 2 updates (#720) (dependabot[bot])
  • 9e27976 — feat: support ctrl + arrow keys (#673) (sectore)
  • bae3eb0 — feat: add viewport scroll commands to tapes (#707) (joshka-oai)
  • 9c80359 — fix: lint issues (aymanbagabas)
  • 55f824a — fix(docker): remove ubuntu font which is no longer available in alpine (aymanbagabas)
  • b8a1d86 — fix(docker): update env variables to use '=' (aymanbagabas)

🔒Security observations

  • High · Potential Command Injection via PTY/Shell Execution — command.go, evaluator.go, creack/pty dependency. The codebase uses 'github.com/creack/pty' for pseudo-terminal operations and appears to execute user-provided commands (based on command.go and evaluator.go). Without proper input validation and sanitization, this could lead to command injection vulnerabilities when processing .tape files or user input. Fix: Implement strict input validation for all commands before execution. Use allowlists for permitted commands, escape shell metacharacters, and consider using exec.Command with explicit argument arrays rather than shell interpretation.
  • High · Insecure SSH Key Generation and Storage — SSH-related components, keygen dependency. The project uses 'github.com/charmbracelet/keygen' and 'github.com/charmbracelet/ssh' for SSH operations. If SSH keys are generated or stored without proper file permissions (0600) or in predictable locations, this could expose private keys. Fix: Ensure SSH keys are generated with secure permissions (0600), stored in secure locations ($HOME/.ssh), and never logged or exposed in output. Implement proper key rotation and validate all SSH key operations.
  • High · Browser Automation Security Risk (Chromium/Headless) — go-rod/rod dependency. The dependency 'github.com/go-rod/rod' is used for browser automation (recording web interactions). Headless browser automation can be exploited to execute arbitrary JavaScript or access sensitive data if not properly sandboxed. Fix: Ensure go-rod is used only with trusted input. Implement sandboxing, disable dangerous features (file:// protocol access, arbitrary script execution), validate all URLs, and run browser instances with minimal privileges. Consider using a separate container/process with restricted permissions.
  • Medium · Exposed SSH Server via WISH Framework — Dockerfile, SSH-related components, wish dependency. The project uses 'github.com/charmbracelet/wish' for SSH server functionality and exposes ports in the Dockerfile. Without proper authentication and access controls, the SSH server could be exploited for unauthorized access. Fix: Implement strong authentication mechanisms, use key-based authentication only, restrict SSH access to specific users/IPs, run the SSH server on non-standard ports if possible, and ensure all connections are properly logged and monitored.
  • Medium · Clipboard Access Without User Consent — atotto/clipboard dependency. The 'github.com/atotto/clipboard' dependency allows clipboard read/write operations. Recording user sessions may unintentionally capture or expose clipboard contents without explicit consent. Fix: Implement explicit user consent mechanisms before accessing clipboard. Document clipboard interaction clearly, provide opt-out mechanisms, and sanitize any clipboard data that might be recorded. Consider implementing warnings when clipboard access is detected.
  • Medium · Outdated Cryptography Dependencies — go.mod - golang.org/x/crypto v0.49.0. The 'golang.org/x/crypto' version is pinned to v0.49.0, which may contain known vulnerabilities. The project uses Go 1.25.8 but crypto library should be kept at latest patch versions. Fix: Update golang.org/x/crypto to the latest stable version. Implement automated dependency scanning and updates via dependabot (already partially in place). Run 'go get -u golang.org/x/crypto' regularly.
  • Medium · Insecure Dockerfile Base Image and Alpine Repository Configuration — Dockerfile - lines with 'FROM alpine:latest' and edge repository definitions. The Dockerfile uses 'alpine:latest' without pinned version and configures APK repositories including 'edge' branches. This can lead to inconsistent builds and potential installation of unstable/vulnerable packages. Fix: Pin Alpine version to a specific release (e.g., 'alpine:3.19'). Use only stable repositories and avoid 'edge' in production. Regularly scan base images for vulnerabilities. Consider using '--no-cache' and implementing layer caching optimization.
  • Medium · Missing Security Headers and Input Validation in Web Components — glam. The project includes web-based examples and uses glamour for HTML rendering. Without proper Content Security Policy (CSP) and XSS protection, rendered content could be exploited. Fix: undefined

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · charmbracelet/vhs — RepoPilot