Wie charmbracelet/vhs CLI-Aufnahmen vom Screencast in eine reproduzierbare Build-Pipeline verlegt
Einstieg
Beim Aufräumen eines README für ein internes CLI-Tool letzte Woche bin ich an der üblichen Stelle hängengeblieben: dem Demo-GIF. Vor einem Jahr mit einem Bildschirmrekorder aufgenommen, mit der Schriftgröße meines damaligen Terminals fest eingebrannt. Ein Tippfehler in der zweiten Zeile nervt mich seitdem bei jedem Blick auf die Repo-Seite. Ein neues GIF aufnehmen heißt: Terminal starten, Fenstergröße einstellen, Aufnahme starten, alles flüssig tippen, Ergebnis zurechtschneiden, hochladen. Eine halbe Stunde, von der hinterher nichts reproduzierbar bleibt.
Bei der Google-Suche nach einer Alternative zum Screencast-Workflow bin ich dann an VHS hängengeblieben. „Your CLI home video recorder”, so verkauft sich das Tool. Gemeint ist damit etwas Spezifischeres als jeder Screencast-Rekorder vorher: Tape-Dateien als Quellcode für Terminalaufnahmen. Ein paar Stunden später lag das alte Demo-GIF als .tape im Repo. Der Tippfehler war eine einzige Zeilenkorrektur, und das CI rendert die GIF-Datei neu, sobald jemand das Tape anfasst.
Wenn Du CLI-Tools dokumentierst, demonstrierst oder mit Snapshot-Tests absicherst, lohnt sich der Blick darauf.
Was VHS ist und warum es kein Screencast-Rekorder ist
Der Name VHS ist eine Verbeugung vor dem „Video Home System” der 1970er. Das erklärt auch, warum die Eingangsdateien Tapes heißen und das fertige Format gelegentlich als „Cassette” auftaucht.
Das Werkzeug ist in Go geschrieben, MIT-lizenziert und liegt bei rund 19.000 Stars auf GitHub. Aktuelles Release ist v0.10.0 vom Juni 2025. Der Quellcode steht unter github.com/charmbracelet/vhs.
Der entscheidende Unterschied zum Screencast: Eine Sitzung wird in einer Datei beschrieben, statt live aufgenommen. Diese Datei heißt Tape, hat die Endung .tape und liest sich wie ein Script. Tippe „echo Hallo”, warte eine halbe Sekunde, drücke Enter, warte fünf Sekunden, fertig. VHS startet ein virtuelles Terminal, spielt das Tape ab, zeichnet die Frames bei fester Bildrate auf und schreibt das Ergebnis als GIF, MP4 oder WebM.
Im Hintergrund kombiniert VHS dafür zwei Werkzeuge: ttyd, das ein Terminal als Web-Service bereitstellt, und ffmpeg für das Encoding. Beide müssen auf dem System verfügbar sein, das die Aufnahme rendert.
Installation
Auf einem aktuellen macOS reicht in den meisten Fällen Homebrew:
brew install vhsBrew zieht ttyd und ffmpeg als Dependencies mit. Den Headless-Browser, den VHS zusätzlich braucht, musst Du separat installieren (Details im Abschnitt „Setup-Stolpersteine auf macOS”). Auf Linux hängt es am Paketmanager. Arch und Void haben VHS in den offiziellen Repos. Auf Debian und Ubuntu liegt das Charm-Repo bei repo.charm.sh, ttyd musst Du dort separat aus den GitHub-Releases von tsl0922/ttyd nachziehen. Unter Nix gibt es das Paket im nixpkgs-Channel, unter Windows ist winget install charmbracelet.vhs oder scoop install vhs der direkte Weg.
Wenn Du Dir das Abhängigkeits-Hantieren sparen willst, nimm das Docker-Image:
docker run --rm -v $PWD:/vhs ghcr.io/charmbracelet/vhs demo.tapeDas Image bringt VHS, ttyd und ffmpeg mit, mountet das aktuelle Verzeichnis als /vhs und schreibt das Resultat dort hin. Mit Podman geht derselbe Aufruf identisch, nur mit podman statt docker davor. Für CI ist das der entspannteste Weg, für lokales Entwickeln stört nur die Container-Latenz.
Für Go-Leute geht es klassisch:
go install github.com/charmbracelet/vhs@latestDas funktioniert, ersetzt aber nicht die ttyd/ffmpeg-Installation auf Systemebene.
Dein erstes Tape
Lege ein neues Tape an:
vhs new demo.tapeDas Tape liegt im aktuellen Verzeichnis und enthält ein paar Demo-Kommandos. Öffne es in Deinem Editor und ersetz den Inhalt durch ein Minimal-Beispiel:
# Wohin geht das GIF?
Output demo.gif
# Terminalgröße und Schrift festlegen.
Set FontSize 22
Set Width 1100
Set Height 500
Set Theme "Catppuccin Frappe"
# Setup laufen lassen, nicht zeigen.
Hide
Type "clear"
Enter
Show
# Tatsächliche Demo.
Type "echo 'Tape statt Screencast'"
Sleep 500ms
Enter
Sleep 2s
Type "ls -lah"
Sleep 300ms
Enter
Sleep 3sRender das Ganze:
vhs demo.tapeVHS startet die virtuelle Sitzung, spielt das Tape ab und schreibt demo.gif neben das Tape. Du hast jetzt eine reproduzierbare Aufnahme. Stört Dich der Output-Text, änderst Du die Type-Zeile, rufst vhs demo.tape noch einmal auf und hast das neue GIF. Kein Schnitt, kein Re-Recording, kein „diesmal flüssig tippen”.

Die Tape-Sprache, kompakt
Tape-Dateien bestehen aus wenigen Befehls-Typen. Du kommst mit einem Dutzend Kommandos durch fast jede README-Demo.
Output legt das Ziel fest und darf mehrfach vorkommen. Drei Zeilen am Anfang erzeugen GIF, MP4 und WebM in einem Lauf. Wenn Du Einzelbilder brauchst, gib ein Verzeichnis an: Output frames/. VHS schreibt dann eine PNG-Sequenz.
Set konfiguriert das Terminal. Schrift, Größe, Padding, Theme, Framerate, Cursor-Blinken, Window-Bar (etwa der bunte Punkte-Header oben links), Border-Radius. Alle Settings müssen vor dem ersten echten Kommando stehen, sonst werden sie ignoriert. Die komplette Theme-Liste bekommst Du übrigens nicht aus der Doku, sondern direkt aus dem Tool selbst: vhs themes zeigt die mitgelieferten Farbschemata an, von Catppuccin über Gruvbox bis Solarized in mehreren Varianten. Einzige Ausnahme ist TypingSpeed, das auch mitten im Tape umgeschaltet werden darf. Per Type@500ms "..." geht das sogar für ein einzelnes Kommando.
Type simuliert Tastatureingaben. Enter, Tab, Backspace, Space, die Pfeiltasten und PageUp/PageDown sind eigene Befehle, optional mit Wiederholzahl: Backspace 18 löscht achtzehn Zeichen. Ctrl+R oder Ctrl+Alt+Shift+T steht für Modifier-Kombinationen.
Sleep wartet eine feste Zeit, Wait wartet auf ein Muster im Output. Letzteres ist der wichtigste Befehl, sobald Du etwas aufnimmst, dessen Dauer Du nicht kennst, etwa einen Build, einen Spinner oder einen Test-Lauf. Das hier wartet bis zu zehn Sekunden, bis der String „Done” in der letzten Zeile auftaucht:
Wait+Line@10s /Done/Der Default-Scope ist Line und prüft nur die letzte Zeile. Mit Wait+Screen schaust Du auf den gesamten sichtbaren Inhalt. Ein realistischer Mini-Block sieht so aus:
Type "make build"
Enter
Wait+Screen@2m /Build complete/
Type "./bin/app --help"
Enter
Sleep 3sDie Aufnahme stoppt nicht stur nach Sleep, sondern erst, wenn der Build wirklich fertig ist. Genau dieser Befehl macht Tapes für längere Workflows belastbar, ohne dass Du Pufferzeiten überdimensionieren musst.
Hide und Show schalten die Aufnahme aus und wieder ein, ohne das virtuelle Terminal anzuhalten. Damit kannst Du im Tape den Build laufen lassen, Pfade anlegen, ein Binary bauen, ohne dass der Zuschauer es sieht. Nach der Demo räumst Du genauso unsichtbar wieder auf.
Screenshot zieht ein PNG an einer beliebigen Stelle. Praktisch, wenn Du dieselbe Demo gleichzeitig für ein README-Bild verwendest.
Source zieht ein anderes Tape ein. Du kannst Deine Default-Einstellungen (Theme, Schrift, Größe) in eine style.tape legen und in jedem Demo-Tape mit Source style.tape einbinden. Spart das Copy-and-Paste der Set-Zeilen.
Aufnehmen statt schreiben
Wenn Dir das Tippen des Tapes zäh vorkommt, geht es auch umgekehrt. VHS kann mitschneiden, was Du tippst, und das Tape daraus schreiben.
vhs record > cassette.tapeDu landest in einer Shell, die jede Eingabe mit Zeitstempel protokolliert. Tipp die Demo so, wie Du sie zeigen willst, schließe die Shell mit exit. VHS schreibt Dir ein Tape, das Deine Eingaben mit Sleep-Befehlen dazwischen rekonstruiert. Lass das nicht ungeprüft stehen, sondern öffne das Tape und entferne Setup-Kommandos, falsche Tippfehler und Sleep-Werte, die zu lang sind. Als Skelett spart der Record-Modus trotzdem eine Menge Tipparbeit.
CI-Integration und golden-file-Tests
Der eigentliche Gewinn entsteht, wenn das Tape im Repo liegt und CI das GIF baut. Charm Bracelet pflegt eine offizielle GitHub Action:
charmbracelet/vhs-actionDie Action installiert VHS, ttyd und ffmpeg und rendert die im Workflow angegebenen Tapes. Den Commit der neuen Artefakte hängst Du als nachgelagerten Schritt im Workflow an, etwa über git-auto-commit-action oder ein eigenes git commit. Wenn jemand das Tape ändert, ändert sich auch das GIF im README. Niemand muss mehr sagen: „ich nehme das nochmal neu auf, wenn ich Zeit habe”.
Der zweite große Anwendungsfall für CI sind golden-file-Tests, vor allem für TUI-Projekte. TUIs sind Programme mit interaktiver Terminal-Oberfläche, also Werkzeuge wie htop, lazygit oder k9s. Sie sind schwer per Unit-Test abzudecken, weil sich ihr Bildschirminhalt permanent ändert. Bei einem golden-file-Test legst Du den erwarteten Output einmal als Referenzdatei ins Repo, und jeder Testlauf vergleicht die aktuelle Ausgabe gegen diese Datei. Statt eines GIFs schreibt VHS Dir den Terminalinhalt als reinen Text raus.
Output golden.asciiDie Datei landet im Git, und jeder Lauf des Tapes vergleicht das aktuelle Ergebnis mit dem committeten Stand. Verändert ein PR die Ausgabe, weicht das golden-file ab. Der CI-Run schlägt fehl, bevor das Verhalten beim Nutzer ankommt. Für CLI-Werkzeuge, die formatierten Output produzieren, ist das eine erstaunlich pragmatische Test-Methode.
Self-Hosting per SSH-Server
Wenn Du VHS auf einem Server laufen lässt und die Tapes von Deinem Notebook aus dort rendern willst, startet vhs serve einen SSH-Server. Der Server liest Tapes aus dem Standard-Input und schreibt das Ergebnis nach Standard-Output.
ssh vhs.example.com < demo.tape > demo.gifKonfiguriert wird der Server über Umgebungsvariablen: VHS_PORT, VHS_HOST, VHS_KEY_PATH, VHS_AUTHORIZED_KEYS_PATH. Per Default bindet der Server an 127.0.0.1, ist also außerhalb des Rechners gar nicht erreichbar. Für eine echte Render-Box im Netz setzt Du VHS_HOST auf die gewünschte Bind-Adresse und pflegst eine authorized_keys-Datei. Ohne diese Liste ist der Endpunkt öffentlich für jeden, der den Port erreicht. Das Ergebnis ist eine Render-Box, auf der die Abhängigkeiten zentral liegen, und Deine Entwicklungsrechner brauchen weder ttyd noch ffmpeg lokal.
Wer ohne eigenen Server arbeiten will, hat mit vhs publish eine bequeme Variante. Der Befehl lädt das fertige GIF nach vhs.charm.sh und gibt Browser-, HTML- und Markdown-Links zurück. Praktisch, wenn Du Tapes mit Kollegen teilst. Ungeeignet, sobald die Inhalte sensibel sind.
Setup-Stolpersteine auf macOS
Bei mir lief das erste vhs demo.tape nicht durch. Der Fehler kam direkt nach dem Start:
could not open ttyd: navigation failed: net::ERR_CONNECTION_REFUSED
recording failedDie README sagt klar, dass ttyd und ffmpeg im PATH liegen müssen. Was nicht erwähnt wird: VHS startet zusätzlich einen Headless-Browser, der ttyd über localhost ansurft. Ohne installiertes Chrome oder Chromium läuft VHS nicht. Auf einem frischen Mac fehlt das oft, und die Fehlermeldung benennt die Ursache nicht klar.
Die Reparatur ist eine Zeile, sobald die Diagnose steht:
brew install --cask google-chromeBrew-Chromium (brew install chromium) ist seit Anfang 2026 als deprecated markiert, weil das Binary die Gatekeeper-Prüfung nicht besteht. Wenn Du Chrome nicht als Alltagsbrowser haben willst, ist das Docker-Image der einzige saubere Ausweg, weil dort ein passender Headless-Chromium im Image liegt:
docker run --rm -v $PWD:/vhs ghcr.io/charmbracelet/vhs demo.tapeDrei weitere Punkte stören weniger beim ersten Start. Sie fallen erst beim Feinschliff auf.
Erstens, Schriften: VHS rendert das Terminal mit der via Set FontFamily angegebenen Schrift. Liegt die Schrift nicht im System-Font-Verzeichnis, fällt VHS still auf die Default-Schrift zurück. Wenn Dir ein gerendertes GIF anders aussieht als auf dem Entwicklungsrechner, ist die Schriftarten-Pfad-Konfiguration der erste Verdächtige.
Zweitens, Dateigrößen: GIFs werden schnell groß. Ein zwanzigsekündiges Tape in 1200×600 bei 60 fps liegt schnell im zweistelligen Megabyte-Bereich. Für README-Demos lohnt es sich, Set Framerate 30 zu setzen und Set PlaybackSpeed über 1.0 anzuheben, wenn die Demo ruhig schneller laufen darf. WebM und MP4 sind kompakter, GitHub-READMEs zeigen aber weiterhin GIFs am zuverlässigsten inline.
Drittens, Timeouts: Wait ohne expliziten Timeout bricht nach fünfzehn Sekunden ab. Bei längeren Build- oder Test-Schritten musst Du den Timeout explizit setzen, etwa Wait@2m /Build complete/. Ohne diese Anpassung schlägt das Tape genau dort fehl, wo es Stabilität beweisen soll.
Schluss
Es gibt eine kleine, aber spürbare Verlagerung, sobald die Demo-GIFs Deiner Projekte zu Tapes werden. Jede README-Aufnahme wird ein PR. Die Diskussion verschiebt sich von der Bildqualität auf die Reihenfolge der Kommandos. Eine Demo reproduzierst Du in zehn Sekunden statt in einer halben Stunde. Im Git landet der Beleg, nicht mehr das Beweisbild.
Lege Dir heute eine style.tape an mit Deinen Default-Settings, mach ein Tape für die Demo, die Dich aktuell am meisten nervt, und häng das CI dran. Dann hörst Du auf, an Screencasts zu schneiden. Demos werden gewartet wie Code.
Links und weiterführende Quellen
Projekt und Doku
- VHS-Repository auf GitHub: github.com/charmbracelet/vhs
- THEMES.md mit kompletter Theme-Liste: github.com/charmbracelet/vhs/blob/main/THEMES.md
- Beispiel-Tapes im Repo: github.com/charmbracelet/vhs/tree/main/examples
Integration
- Offizielle GitHub Action: github.com/charmbracelet/vhs-action
Hosting
- Charms eigener Render- und Hosting-Endpunkt: vhs.charm.sh

