Create animated terminal demo GIFs for GitHub READMEs with Claude Code. Describe what the terminal should show in natural language and get a polished GIF.
SaferSkills independently audited command-giffer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Generate animated terminal demo GIFs from natural language descriptions.
Parse $ARGUMENTS to extract:
$ARGUMENTS)demo.gif in the current working directory)820)400)If $ARGUMENTS is empty or contains no description, ask the user what they want the terminal to show.
This is the creative step. Interpret the user's natural language description and design a convincing terminal animation. Decide:
$ prompt (e.g., pytest tests/ -v, npm install, git status)Use these CSS classes to color output lines convincingly:
| Class | Color | Use for |
|---|---|---|
green | #7ee787 | Success, PASSED, checkmarks, "done" messages |
red | #f85149 | Failures, FAILED, errors |
yellow | #d29922 | Warnings, deprecation notices |
cyan | #79c0ff | Paths, URLs, links |
purple | #d2a8ff | Bullet points, list markers, section names |
dim | #8b949e | Metadata, timestamps, version info |
bold white | #e6edf3 | Headers, section dividers, summaries |
white | #e6edf3 | Normal text |
Combine classes: bold green for emphasized success, bold red for emphasized failure.
pytest: Header line → platform info (dim) → collected N items → blank → test lines (green PASSED / red FAILED) → blank → summary line (bold green if all pass, bold red if failures).
npm install: "added N packages" → timing → optional audit line (dim).
npm test / npm run: Script echo (dim) → tool output → summary.
git status: "On branch main" → section headers (bold white) → file listings with color (green for new, red for modified, cyan for paths).
git log: Commit hashes (yellow) → author/date (dim) → messages (white).
cargo / rustc: Compiling lines (green) → Finished (bold green) or error (bold red).
Generic: Use sensible defaults — dim metadata, green for success, red for errors, bold white for headers.
TYPING_SPEED: 50-70ms per character (looks natural)INITIAL_DELAY: 400-600ms (pause before typing starts)POST_COMMAND_DELAY: 400-800ms (simulates tool startup)POST_ANIMATION_DELAY: 1200-1800ms (hold final frame for readability)Keep total animation under 8 seconds. If there are many output lines, reduce per-line delays.
Check that all required tools are available. Run these checks:
# Check ffmpeg availability
which ffmpeg 2>/dev/null || command -v ffmpeg 2>/dev/null || ffmpeg -version 2>/dev/nullIf ffmpeg is not found, check common installation locations:
/opt/homebrew/bin/ffmpeg, /usr/local/bin/ffmpeg/usr/bin/ffmpeg, /snap/bin/ffmpegC:/ProgramData/chocolatey/bin/ffmpeg.exe, C:/ffmpeg/bin/ffmpeg.exeIf still not found, ask the user before installing:
"ffmpeg is required but not found. Would you like me to install it? (brew install ffmpeg / choco install ffmpeg / sudo apt install ffmpeg)"
Do NOT install without user confirmation.
Test that the Playwright MCP browser is available by calling mcp__playwright__browser_snapshot. If it returns an error about browser not being installed, call mcp__playwright__browser_install to install it.
python3 --version 2>/dev/null || python --version 2>/dev/nullPython is needed for python -m http.server. If not available, the skill cannot proceed — inform the user.
Read the reference template to understand the structure:
Read file: <skill-dir>/references/template.htmlWhere <skill-dir> is the directory containing this SKILL.md file. Use Glob to locate it if needed:
Glob pattern: **/command-giffer/references/template.htmlCreate a temporary working directory:
TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'command-giffer')
echo $TMPDIROn Windows (Git Bash), if mktemp fails, use:
TMPDIR="$TEMP/command-giffer-$$"
mkdir -p "$TMPDIR"
echo "$TMPDIR"Write a new HTML file to $TMPDIR/animation.html based on the template structure, but with:
html, body: html, body {
width: WIDTHpx;
height: HEIGHTpx;
overflow: hidden;
}WINDOW_TITLE variableKeep the animation engine code (runAnimation, typeText, showOutputLine, sleep, createLine) exactly as-is from the template. Only modify the configurable data section.
Start a Python HTTP server on a free port, serving the temp directory:
# Find a free port using Python
PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()" 2>/dev/null || python -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()")
cd "$TMPDIR" && python3 -m http.server $PORT --bind 127.0.0.1 &
# or on Windows / if python3 not available:
# cd "$TMPDIR" && python -m http.server $PORT --bind 127.0.0.1 &
# Wait for server to start
sleep 1This is the critical step. Use mcp__playwright__browser_run_code to create a new browser context with video recording enabled. The standard Playwright MCP tools cannot set recordVideo — only browser_run_code can do this.
Call mcp__playwright__browser_run_code with this code (adjust PORT, WIDTH, HEIGHT, and TMPDIR_PATH):
async (page) => {
const browser = page.context().browser();
const context = await browser.newContext({
viewport: { width: WIDTH, height: HEIGHT },
recordVideo: {
dir: 'TMPDIR_PATH',
size: { width: WIDTH, height: HEIGHT }
}
});
const recPage = await context.newPage();
await recPage.goto('http://127.0.0.1:PORT/animation.html');
await recPage.waitForFunction(() => document.title === 'ANIMATION_DONE', null, { timeout: 30000 });
await recPage.waitForTimeout(1000);
await context.close();
return 'Recording complete';
}Important notes:
require('playwright') is not available in the MCP execution contextrecordVideo.size should match viewport dimensions exactlyTMPDIR_PATH even on Windows (for JS string)\ with / in the pathANIMATION_DONE title which the animation engine sets when donewaitForTimeout(1000) ensures the last frame is capturedcontext.close() finalizes the video file — do NOT call browser.close() as the browser is shared with the MCP serverAfter recording, find the video file:
# On Windows, Git Bash /tmp may differ from the Windows C:/tmp path
# Try both the variable and find to be safe
WEBM_FILE=$(ls "$TMPDIR"/*.webm 2>/dev/null | head -1)
if [ -z "$WEBM_FILE" ]; then
WEBM_FILE=$(find "$(cygpath -w "$TMPDIR" 2>/dev/null || echo "$TMPDIR")" -name "*.webm" 2>/dev/null | head -1)
fi
echo "Video: $WEBM_FILE"The video file will be in the temp directory with a random name like a1b2c3d4.webm.
Use a two-pass ffmpeg palette method for high-quality GIF output:
WEBM_FILE=$(ls "$TMPDIR"/*.webm 2>/dev/null | head -1)
if [ -z "$WEBM_FILE" ]; then
WEBM_FILE=$(find "$(cygpath -w "$TMPDIR" 2>/dev/null || echo "$TMPDIR")" -name "*.webm" 2>/dev/null | head -1)
fi
OUTPUT_PATH="the --output path"
PALETTE="$TMPDIR/palette.png"
# Pass 1: Generate optimized palette
ffmpeg -i "$WEBM_FILE" -vf "fps=15,scale=WIDTH:-1:flags=lanczos,palettegen=max_colors=128:stats_mode=diff" -y "$PALETTE"
# Pass 2: Create GIF using the palette
ffmpeg -i "$WEBM_FILE" -i "$PALETTE" -lavfi "fps=15,scale=WIDTH:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=floyd_steinberg:diff_mode=rectangle" -y "$OUTPUT_PATH"Where WIDTH is the --width value from Step 1 (matching the recording viewport).
If the GIF is larger than 5MB, retry with max_colors=64 and fps=12.
# Kill the HTTP server
kill %1 2>/dev/null || true
# If kill %1 doesn't work (e.g., different shell session), find and kill by port:
# lsof -ti:$PORT 2>/dev/null | xargs kill 2>/dev/null || true # macOS/Linux
# On Windows Git Bash, the backgrounded process is usually cleaned up by kill %1
# Remove temp directory
rm -rf "$TMPDIR"Verify the output GIF exists and is non-empty:
ls -la "$OUTPUT_PATH"Print a summary to the user:
GIF created successfully!
Output: ./demo.gif
Size: 245 KB
Dimensions: 820 x 400
Add to your README:
Include the actual file size (formatted in KB or MB) and the actual dimensions.
If the GIF file size exceeds 10MB, warn the user that GitHub has display issues with large GIFs and suggest reducing dimensions or line count.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.