How To

Record a Linux Terminal Session and Convert It to GIF

A terminal recording is a text file, not a video. That is the whole reason to make one. A two minute session lands in well under a kilobyte, replays at full resolution in any terminal, stays greppable, and becomes a GIF only when you actually need one for a README or a ticket. Screen capture software gives you none of that.

Original content from computingforgeeks.com - post 2974

Four tools cover this on Linux: the script command that is already installed, asciinema for the recording format everyone else reads, agg for turning those recordings into GIF files, and VHS for scripting a terminal demo that renders the same way every time. This guide covers all four, including the version changes that broke older instructions. Everything below was run on Ubuntu 24.04.4 (kernel 6.8.0-106) in September 2026 with asciinema 3.2.1, agg 1.9.0, VHS 0.12.0 and 0.11.0, and terminalizer 0.12.0.

Which Terminal Recorder to Use

Use asciinema plus agg. It is the only combination here that installs as two static binaries, needs no browser, no Node.js and no X server, and renders a GIF in a tenth of a second. Reach for the others only when their specific strength matters.

ToolUse it whenNeedsMakes a GIF
script + scriptreplayYou cannot install anything, or you need an auditable plain text logNothing, ships with util-linuxNo, not directly
asciinema + aggDefault choice for documentation, bug reports and README demosTwo static binariesYes, via agg
VHSThe demo must be scripted and reproducible in CIttyd, ffmpeg and a bundled ChromiumYes, natively
terminalizerNever on a server. Unmaintained since 2024Node.js, Electron and an X serverYes, in theory
Tested on Ubuntu 24.04.4 in September 2026. The footprint and render time figures behind this table are further down.

Record a Session With script, No Install Required

Every Linux system already has a terminal recorder. script is part of util-linux, so it is on the box before you log in, which makes it the only option on a hardened host where you cannot add packages. It writes two files: the raw terminal output, and a timing file that lets you replay it at the original speed.

script -q -c "./deploy.sh" --timing=deploy.timing deploy.typescript

Drop the -c flag to record an interactive shell instead, and press Ctrl+D when you are finished. Either way you end up with a pair of very small files:

-rw-rw-r-- 1 ubuntu ubuntu  72 Sep 19 11:13 deploy.timing
-rw-rw-r-- 1 ubuntu ubuntu 437 Sep 19 11:13 deploy.typescript

The timing file is two columns, the delay in seconds and the number of bytes written after that delay. The typescript is the terminal output verbatim, prefixed with a header line naming the command and the start time. Both are plain text, so a recording survives grep, diff and version control without any tooling.

Replay it with scriptreplay. util-linux 2.39 prefers the newer flag names, though the older --timing and --typescript forms still work:

scriptreplay --log-out deploy.typescript --log-timing deploy.timing --divisor 4

--divisor 4 plays back at four times speed, which is what you want when reviewing a long session before turning it into something shareable. --maxdelay 2 caps every pause at two seconds, so the twenty minutes you spent reading a man page mid-session does not end up in the replay.

Because script needs no install, it is also the usual way to log privileged sessions for audit. Putting it behind an SSH ForceCommand, or invoking it from a bastion host’s login shell, captures everything an operator typed into a file they cannot edit. If you are wiring that up, SSH tunnels and jump host access covers the connection side.

Install asciinema on Linux

asciinema is on version 3, a full rewrite in Rust, but most distributions still package version 2. Check what your package manager actually offers before following any guide written against the older release, because the two are not the same program.

DistributionPackaged versionInstall
Ubuntu 24.04 LTS2.4.0-1apt install asciinema
Debian 13 (trixie)2.4.0-1apt install asciinema
Fedora 443.0.0-5.fc44dnf install asciinema
RHEL 9, Rocky 9, Alma 92.2.0-4.el9 (EPEL)dnf install epel-release asciinema
RHEL 10, Rocky 10, Alma 10Not packaged in EPELDownload the binary
Any distribution3.2.1 (current)Download the binary
Package versions checked against the Ubuntu, Debian, Fedora and EPEL repositories on 19 September 2026.

Fedora is the only mainstream distribution shipping version 3 today. On everything else, grab the static binary from the release page. It has no runtime dependencies:

curl -sLo asciinema https://github.com/asciinema/asciinema/releases/download/v3.2.1/asciinema-x86_64-unknown-linux-gnu  # https://github.com/asciinema/asciinema/releases
chmod +x asciinema
sudo mv asciinema /usr/local/bin/asciinema

Confirm which one is first on your PATH, since a distro package in /usr/bin and a binary in /usr/local/bin will happily coexist and quietly disagree:

asciinema --version

The version string is the only thing that tells the two generations apart:

asciinema 3.2.1

A musl build is published alongside the glibc one for Alpine and other musl systems, and there are darwin builds if you also work on a Mac.

Record, Replay and Stream With asciinema

Recording drops you into a new shell where everything is captured. Exit the shell to stop:

asciinema rec demo.cast

For a scripted, repeatable capture, pass the command directly and the recording stops when it finishes. --overwrite saves you from a prompt on every re-run, which matters when you are iterating:

asciinema rec demo.cast -c "./deploy.sh" --overwrite

Two flags do most of the work in practice. -i 2 clamps idle time to two seconds so thinking pauses do not bloat the replay, and --window-size 92x14 pins the terminal geometry so the recording does not inherit whatever size your window happened to be. Version 3 also adds -I to capture keystrokes as well as output, and --headless to record without attaching to a terminal at all, which is what you want from a cron job or a CI step.

Play a recording back in place:

asciinema play demo.cast --speed 2

Space pauses and resumes, . steps through events while paused, and ] jumps to the next marker. Pair that with --pause-on-markers and a recording becomes a slide deck you can walk a room through. Version 3 adds a third mode on top of record and play. stream broadcasts the session live over a local HTTP server, so a colleague on the same network can watch a build or a migration as it happens without you sharing a screen:

asciinema stream --local 0.0.0.0:8080

Leave the address off and it binds an ephemeral port on localhost instead. --remote pushes the same stream to an asciinema server for a public URL. This is a much lighter answer to “can you show me what you’re seeing” than a video call, and it pairs well with running the work inside a tmux session so the session survives a dropped connection.

What Changed in the asciicast v3 Format

Version 3 writes a new recording format, and the header is where you can see it immediately. Version 2 declared the terminal size as top level keys:

{"version": 2, "width": 80, "height": 24, "timestamp": 1789815094, "env": {"SHELL": "/bin/bash", "TERM": null}}

Version 3 nests them under term and records the command that was captured:

{"version":3,"term":{"cols":80,"rows":24},"timestamp":1789815109,"command":"./demo.sh","env":{"SHELL":"/bin/bash"}}

The second change is easier to miss and matters more if you parse these files yourself. Version 2 timestamps each event as seconds since the start of the recording, so they climb:

[0.00225, "o", "checking node health\r\n"]
[0.804772, "o", "Linux 6.8.0-106-generic x86_64\r\n"]
[1.607254, "o", "               total        used        free\r\n"]
[2.409436, "o", "/dev/sda1        22G  2.6G   20G  12% /\r\n"]

Version 3 records the interval since the previous event instead, so the numbers stay flat:

[0.001, "o", "checking node health\r\n"]
[0.802, "o", "Linux 6.8.0-106-generic x86_64\r\n"]
[0.803, "o", "               total        used        free\r\n"]
[0.803, "o", "/dev/sda1        22G  2.6G   20G  12% /\r\n"]

Anything reading these files naively will play a v3 recording at the wrong speed. agg is not affected. Version 1.7.0 and version 1.9.0 each rendered v2 and v3 copies of the same session to a six frame, 7.01 second GIF, so you do not need to upgrade agg just because you upgraded asciinema.

error: 2 values required by ‘<FILE> <FILE>…’; only 1 was provided

This is the one command that breaks on upgrade. In version 2, asciinema cat demo.cast printed a recording’s output as plain text, which is how people piped recordings into logs and diffs. Version 3 repurposed cat to concatenate several recordings into one, so a single argument is now an error.

The replacement is convert, which selects the txt format automatically from the output extension:

asciinema convert demo.cast demo.txt

You get the session output with colours and control sequences stripped, the same thing old cat produced:

checking node health
Linux 6.8.0-106-generic x86_64
               total        used        free        shared  buff/cache   available
Mem:           5.8Gi       460Mi       4.2Gi       1.0Mi       1.4Gi       5.3Gi

The same subcommand downgrades a recording when some other tool in your pipeline only understands the older format. Pass the format explicitly, because the default output is v3:

asciinema convert demo.cast legacy.cast -f asciicast-v2

raw is the fourth format, giving you the terminal output including control sequences but with no timing. convert also accepts an HTTP URL as input, so you can pull a recording straight off a server without downloading it first.

Convert a Recording to GIF With agg

agg is the asciinema GIF generator, a separate static binary from the same project. Install it the same way:

curl -sLo agg https://github.com/asciinema/agg/releases/download/v1.9.0/agg-x86_64-unknown-linux-gnu  # https://github.com/asciinema/agg/releases
chmod +x agg
sudo mv agg /usr/local/bin/agg

Converting takes two arguments and no configuration:

agg demo.cast demo.gif

A five second recording rendered in a tenth of a second across three consecutive runs and produced a 19,320 byte GIF. Output is deterministic, so the same cast file and flags give a byte identical GIF every time, which is what makes agg safe to run in a documentation build. Here is the full loop, recorded with asciinema and rendered with agg:

Terminal GIF showing asciinema rec capturing a session and agg converting the cast file into a GIF
The asciinema and agg pipeline, recorded with asciinema 3.2.1 and rendered by agg 1.9.0.

Geometry and font size are worth setting explicitly so the GIF is not at the mercy of the terminal you recorded in. --cols and --rows override the recorded size, and the default font size is 16 pixels:

agg demo.cast demo.gif --cols 92 --rows 14 --font-size 15 --idle-time-limit 2

Theme choice is the other lever. agg 1.9.0 ships thirteen values for --theme, well beyond the handful older guides list: asciinema, dracula, github-dark, github-light, kanagawa, kanagawa-dragon, kanagawa-light, monokai, nord, solarized-dark, solarized-light, gruvbox-dark and custom. The palette changes the file size, because a GIF is limited to 256 colours and a busier palette compresses worse:

ThemeGIF size
asciinema18,652 bytes
github-light18,763 bytes
monokai18,884 bytes
dracula19,320 bytes
gruvbox-dark19,515 bytes
nord19,559 bytes
solarized-dark20,136 bytes
The same five second recording rendered by agg 1.9.0 at default geometry, one run per theme.

The flag that saves the most work is --select, added in recent releases. It renders part of a recording instead of all of it, taking a time range, a percentage, an event index, or a marker you set during recording:

agg demo.cast build.gif --select marker:build..marker:test

That means one long recording can produce several short, focused GIFs without re-recording anything. --speed, --fps-cap (30 by default), --no-loop and --last-frame-duration cover the rest of the playback behaviour.

A recording that already exists as script output can still reach a GIF. Record the replay, then render that:

asciinema rec replay.cast -c "scriptreplay --log-out deploy.typescript --log-timing deploy.timing --divisor 2"
agg replay.cast replay.gif

That produced a 598 byte cast and a 19,375 byte GIF from the original 509 bytes of script output, so an audit log captured months ago on a box with nothing installed can still become a diagram in a postmortem.

Generate Terminal GIFs From a Tape File With VHS

VHS inverts the workflow. Instead of recording yourself and converting the result, you write a .tape script describing what should be typed, and VHS drives a real terminal to produce the GIF. Nobody fluffs a take, the output is reproducible, and the tape file diffs cleanly in review. This is why so many CLI projects use it for the demo at the top of their README.

It needs ffmpeg and ttyd on the host, then the release package:

sudo apt install -y ffmpeg ttyd
curl -sLo vhs.deb https://github.com/charmbracelet/vhs/releases/download/v0.11.0/vhs_0.11.0_amd64.deb  # https://github.com/charmbracelet/vhs/releases
sudo dpkg -i vhs.deb

On RHEL, Rocky and AlmaLinux the same release page carries an .rpm, and ffmpeg comes from RPM Fusion there rather than the base repositories. Generate a starter tape to see the syntax:

vhs new demo.tape

A working tape is short. Output names the file, Set lines configure the terminal, and the rest is the script:

Output demo.gif
Set FontSize 16
Set Width 900
Set Height 400
Set Theme "Dracula"
Type "uptime"
Enter
Sleep 900ms
Type "df -h / | tail -1"
Enter
Sleep 1200ms

Render it by running the tape:

vhs demo.tape

VHS echoes the parsed tape as it executes each instruction, which makes a syntax mistake obvious straight away:

Terminal GIF showing a VHS tape file being displayed and then rendered into demo.gif
VHS 0.11.0 parsing a tape file and rendering it. The GIF above was itself produced by VHS.

Set Framerate, Set TypingSpeed and Set Padding are the knobs worth learning, and Output frames/ alongside the GIF writes every captured PNG to a directory, which is the fastest way to tell whether a failure happened during capture or during encoding. Budget for larger files than agg produces. The two GIFs above are different clips so it is not a like for like test, but the agg one is 38 KB and the VHS one is 256 KB, because VHS screenshots a real browser rendered terminal at a fixed frame rate instead of drawing only the frames where the screen changed. Set Framerate 10 is the first thing to turn down when a tape produces something too heavy to put on a page.

could not start browser: browser exited unexpectedly before its debugging endpoint was ready

VHS drives a headless Chromium that it downloads itself on first run, into ~/.cache/rod. On Ubuntu 24.04 that first run fails, because 24.04 blocks unprivileged user namespaces by default and Chromium cannot build its sandbox. Running the downloaded binary by hand shows the real cause:

FATAL:zygote_host_impl_linux.cc(126)] No usable sandbox! Update your kernel or see
https://chromium.googlesource.com/chromium/src/+/main/docs/linux/suid_sandbox_development.md
for more information on developing with the SUID sandbox.

Confirm the restriction is the one biting you:

sysctl kernel.apparmor_restrict_unprivileged_userns

A value of 1 means AppArmor is blocking the sandbox:

kernel.apparmor_restrict_unprivileged_userns = 1

Open a sysctl drop-in to turn it off persistently:

sudo vim /etc/sysctl.d/60-apparmor-namespace.conf

Add the single line that re-enables unprivileged user namespaces:

kernel.apparmor_restrict_unprivileged_userns = 0

Apply it with sudo sysctl --system and VHS starts its browser. This is a real reduction in confinement, so do it on the build box that renders your documentation, not on a production host.

VHS 0.12.0 prints “Creating demo.gif” and writes nothing

Pin VHS to 0.11.0 for now. Version 0.12.0, released on 9 September 2026, captures every frame correctly, prints the usual completion message, exits with status 0, and produces no output file at all. There is no error to catch, which makes it poisonous in a pipeline.

Adding a frames directory to the tape proves capture is fine. On a tape that wrote no GIF, the directory still filled with 154 PNG files. Tracing the process shows why nothing gets encoded:

strace -f -e trace=execve -o trace.log vhs demo.tape
grep -c ffmpeg trace.log

The count comes back zero. VHS 0.12.0 never executes ffmpeg. Running the identical tape on the same machine after downgrading gives the expected result:

-rw-rw-r-- 1 ubuntu ubuntu 48323 Sep 19 10:58 vhs.gif

This is tracked upstream as issue 787, with several open pull requests pointing at a render context being cancelled before the encoders finish. Until one lands, install the 0.11.0 package and hold it.

Why Terminalizer No Longer Works on a Headless Server

Terminalizer still appears in every list of terminal recorders, so it is worth stating plainly: the project has had no commit since August 2024, carries no GitHub release at all, and on a current headless Linux server its renderer does not work. Recording is fine. Rendering is where it falls apart, in four steps.

Installed globally with npm, the render step tries to write frames inside its own installation directory:

Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/terminalizer/render/frames'

Re-running under sudo to get that permission trades one failure for another, because the bundled Electron refuses to run as root:

FATAL:electron_main_delegate.cc(294)] Running as root without --no-sandbox is not supported.
Error: Rendering exited with code null

Giving your own user ownership of the install directory clears both, and exposes the real requirement. Electron wants a display:

ERROR:ozone_platform_x11.cc(240)] Missing X server or $DISPLAY
ERROR:env.cc(255)] The platform failed to initialize.  Exiting.

The usual answer is a virtual framebuffer, so sudo apt install xvfb and xvfb-run -a terminalizer render demo. On Ubuntu 24.04 that hung for twelve minutes at zero percent CPU without writing a single frame, then gave up:

ERROR:viz_main_impl.cc(186)] Exiting GPU process due to errors during initialization
Error: ENOENT: no such file or directory, open '/tmp/tmp-15977-dhBH3E7cypHM/0.png'

Four failures and roughly a gigabyte of Node.js and Electron to get nowhere. If you inherited a project that depends on terminalizer, the migration is straightforward: its recordings are YAML and the commands map almost directly onto asciinema rec and agg.

File Size, Render Time and Disk Cost Compared

The same five second session was recorded with each tool on the same Ubuntu 24.04 virtual machine. The recording formats differ by a factor of six before a single frame is rendered:

ToolRecording sizeGIF render timeDisk cost to install
script + scriptreplay509 bytes (437 + 72)Not supported directly0, ships with util-linux
asciinema 3.2.1 + agg 1.9.0528 bytes (asciicast v3)0.10 s23 MB (two static binaries)
VHS 0.11.0Tape script, 157 bytes8.2 s25 MB packages, plus 533 MB Chromium on first run
terminalizer 0.12.03,270 bytes (YAML)Never completedRoughly 1 GB with the Node.js toolchain
Measured on Ubuntu 24.04.4, 4 vCPU and 6 GB RAM, September 2026. Render times are wall clock for the same captured session.

The 533 MB matters more than it looks. VHS downloads its own Chromium build on first run rather than using a system browser, so a container image that renders documentation carries that weight in every layer, and a CI runner pays the download on every cold cache. agg is a single 15 MB binary with no runtime dependencies at all.

Speed matters on the other end. Eighty two times the render time is irrelevant for one GIF and very relevant when a docs build regenerates forty of them on every commit, which is exactly the shape of a documentation as code workflow. If you are choosing tooling for a whole team, the same trade-offs show up across documentation tooling generally.

Keep the Cast File, Not the GIF

Commit the .cast and .tape files to the repository next to the documentation they illustrate, and treat the GIF as a build artifact. A half kilobyte of JSON diffs properly in review, survives a rebrand, and re-renders at any size, theme or frame rate you later decide you want. A 40 KB binary that nobody can regenerate does none of that, and it is the reason so many project READMEs still show a demo of a version that shipped three years ago.

When a recording does need to be a real video rather than a terminal replay, that is the point to reach for OBS Studio instead. Terminal recorders capture text, which is their whole advantage, and it stops being an advantage the moment a browser or a GUI enters the frame.

Keep reading

Fix “apt-key is deprecated. Manage keyring files in trusted.gpg.d instead” Debian Fix “apt-key is deprecated. Manage keyring files in trusted.gpg.d instead” Best Terminal Shell Prompts for Zsh, Bash and Fish Featured Best Terminal Shell Prompts for Zsh, Bash and Fish How To Install Zsh and Oh My Zsh on macOS macos How To Install Zsh and Oh My Zsh on macOS Install Zsh and Oh My Zsh on Linux AlmaLinux Install Zsh and Oh My Zsh on Linux Install OpenStack CLI Client on Ubuntu / Debian / RHEL Cloud Install OpenStack CLI Client on Ubuntu / Debian / RHEL Manage Photon OS from Command Line Interface (CLI) Terminal Manage Photon OS from Command Line Interface (CLI)

Leave a Comment

Press ESC to close