Complete system for streaming asciinema recordings to GitHub with automatic brotli archival. Uses idle-detection for intelligent chunking, zstd for concatenatable streaming compression, and GitHub Actions for final brotli recompression.
When to Use This Skill
Use this skill when:
Setting up real-time backup of asciinema recordings to GitHub
Configuring idle-detection chunking for recordings
Creating orphan branch infrastructure for recording storage
Integrating GitHub Actions for brotli recompression
Purpose: Verify all tools installed, offer self-correction if missing.
/usr/bin/env bash << 'PREFLIGHT_EOF'
# preflight-check.sh - Validates all requirements
MISSING=()
# Check each tool
for tool in asciinema zstd brotli git gh; do
if ! command -v "$tool" &>/dev/null; then
MISSING+=("$tool")
fi
done
if [[ ${#MISSING[@]} -gt 0 ]]; then
echo "Missing tools: ${MISSING[*]}"
echo ""
echo "Install with:"
echo " brew install ${MISSING[*]}"
exit 1
fi
# Check asciinema version (need 3.0+ for Rust version)
ASCIINEMA_VERSION=$(asciinema --version 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1)
if [[ "${ASCIINEMA_VERSION%%.*}" -lt 3 ]]; then
echo "Warning: asciinema $ASCIINEMA_VERSION detected. Version 3.0+ recommended."
echo "Upgrade: brew upgrade asciinema"
fi
echo "All requirements satisfied"
PREFLIGHT_EOF
AskUserQuestion (if tools missing):
AskUserQuestion:
question: "Required tools are missing. How would you like to proceed?"
header: "Preflight Check"
options:
- label: "Install all missing tools (Recommended)"
description: "Run: brew install ${MISSING[*]}"
- label: "Show manual installation commands"
description: "Display commands without executing"
- label: "Continue anyway (may fail later)"
description: "Skip installation and proceed"
Self-Correction: If tools are missing, generate installation command and offer to run it.
Phase 1: GitHub Account Detection
Purpose: Detect available GitHub accounts and let user choose which to use for recording storage.
Detection Sources
Probe these 5 sources to detect GitHub accounts:
Source
Command
What it finds
SSH config
grep -A5 "Host github" ~/.ssh/config
Match directives with IdentityFile
SSH keys
ls ~/.ssh/id_ed25519_*
Account-named keys (e.g., id_ed25519_terrylica)
gh CLI
gh auth status
Authenticated accounts
mise env
grep GH_ACCOUNT .mise.toml
GH_ACCOUNT variable
git config
git config user.name
Global git username
Detection Script
/usr/bin/env bash << 'DETECT_ACCOUNTS_EOF'
# detect-github-accounts.sh - Probe all sources for GitHub accounts
# Uses portable parallel arrays (works in bash 3.2+ and when wrapped for zsh)
ACCOUNT_NAMES=()
ACCOUNT_SOURCES=()
log() { echo "[detect] $*"; }
# Helper: add account with source (updates existing or appends new)
add_account() {
local account="$1" source="$2"
local idx
for idx in "${!ACCOUNT_NAMES[@]}"; do
if [[ "${ACCOUNT_NAMES[$idx]}" == "$account" ]]; then
ACCOUNT_SOURCES[$idx]+="$source "
return
fi
done
ACCOUNT_NAMES+=("$account")
ACCOUNT_SOURCES+=("$source ")
}
# 1. SSH config Match directives
if [[ -f ~/.ssh/config ]]; then
while IFS= read -r line; do
if [[ "$line" =~ IdentityFile.*id_ed25519_([a-zA-Z0-9_-]+) ]]; then
add_account "${BASH_REMATCH[1]}" "ssh-config"
fi
done < ~/.ssh/config
fi
# 2. SSH key filenames
for keyfile in ~/.ssh/id_ed25519_*; do
if [[ -f "$keyfile" && "$keyfile" != *.pub ]]; then
account=$(basename "$keyfile" | sed 's/id_ed25519_//')
add_account "$account" "ssh-key"
fi
done
# 3. gh CLI authenticated accounts
if command -v gh &>/dev/null; then
while IFS= read -r account; do
[[ -n "$account" ]] && add_account "$account" "gh-cli"
done < <(gh auth status 2>&1 | grep -oE 'Logged in to github.com account [a-zA-Z0-9_-]+' | awk '{print $NF}')
fi
# 4. mise env GH_ACCOUNT
if [[ -f .mise.toml ]]; then
account=$(grep -E 'GH_ACCOUNT\s*=' .mise.toml 2>/dev/null | sed 's/.*=\s*"\([^"]*\)".*/\1/')
[[ -n "$account" ]] && add_account "$account" "mise-env"
fi
# 5. git config user.name
git_user=$(git config user.name 2>/dev/null)
[[ -n "$git_user" ]] && add_account "$git_user" "git-config"
# Score and display
log "=== Detected GitHub Accounts ==="
RECOMMENDED=""
MAX_SOURCES=0
for idx in "${!ACCOUNT_NAMES[@]}"; do
account="${ACCOUNT_NAMES[$idx]}"
sources="${ACCOUNT_SOURCES[$idx]}"
count=$(echo "$sources" | wc -w | tr -d ' ')
log "$account: $count sources ($sources)"
if (( count > MAX_SOURCES )); then
MAX_SOURCES=$count
RECOMMENDED="$account"
RECOMMENDED_SOURCES="$sources"
fi
done
echo ""
echo "RECOMMENDED=$RECOMMENDED"
echo "SOURCES=$RECOMMENDED_SOURCES"
DETECT_ACCOUNTS_EOF
AskUserQuestion
AskUserQuestion:
question: "Which GitHub account should be used for recording storage?"
header: "GitHub Account Selection"
options:
- label: "${RECOMMENDED} (Recommended)"
description: "Detected via: ${SOURCES}"
# Additional detected accounts appear here dynamically
- label: "Enter manually"
description: "Type a GitHub username not listed above"
Post-Selection: If user selects an account, ensure gh CLI is using that account:
/usr/bin/env bash << 'POST_SELECT_EOF'
# Ensure gh CLI is authenticated as selected account
SELECTED_ACCOUNT="${1:?Usage: provide selected account}"
if ! gh auth status 2>&1 | grep -q "Logged in to github.com account $SELECTED_ACCOUNT"; then
echo "Switching gh CLI to account: $SELECTED_ACCOUNT"
gh auth switch --user "$SELECTED_ACCOUNT" 2>/dev/null || \
echo "Warning: Could not switch accounts. Manual auth may be needed."
fi
POST_SELECT_EOF
Phase 1.5: Current Repository Detection
Purpose: Detect current git repository context to provide intelligent defaults for Phase 2 questions.
Detection Script
/usr/bin/env bash << 'DETECT_REPO_EOF'
# Detect current repository context for intelligent defaults
CURRENT_REPO_URL=""
CURRENT_REPO_OWNER=""
CURRENT_REPO_NAME=""
DETECTED_FROM=""
# Check if we're in a git repository
if git rev-parse --git-dir &>/dev/null; then
# Try origin remote first
if git remote get-url origin &>/dev/null; then
CURRENT_REPO_URL=$(git remote get-url origin)
DETECTED_FROM="origin remote"
# Fallback to first available remote
elif [[ -n "$(git remote)" ]]; then
REMOTE=$(git remote | head -1)
CURRENT_REPO_URL=$(git remote get-url "$REMOTE")
DETECTED_FROM="$REMOTE remote"
fi
# Parse owner and name from URL (SSH or HTTPS)
if [[ -n "$CURRENT_REPO_URL" ]]; then
if [[ "$CURRENT_REPO_URL" =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then
CURRENT_REPO_OWNER="${BASH_REMATCH[1]}"
CURRENT_REPO_NAME="${BASH_REMATCH[2]%.git}"
fi
fi
fi
# Output for Claude to parse
echo "CURRENT_REPO_URL=$CURRENT_REPO_URL"
echo "CURRENT_REPO_OWNER=$CURRENT_REPO_OWNER"
echo "CURRENT_REPO_NAME=$CURRENT_REPO_NAME"
echo "DETECTED_FROM=$DETECTED_FROM"
DETECT_REPO_EOF
Claude Action: Store detected values (CURRENT_REPO_OWNER, CURRENT_REPO_NAME, DETECTED_FROM) for use in subsequent AskUserQuestion calls. If no repo detected, proceed without defaults.
Phase 2: Core Configuration
Purpose: Gather essential configuration from user.
2.1 Repository URL
If current repo detected (from Phase 1.5):
AskUserQuestion:
question: "Which repository should store the recordings?"
header: "Repository"
options:
- label: "${CURRENT_REPO_OWNER}/${CURRENT_REPO_NAME} (Recommended)"
description: "Current repo detected from ${DETECTED_FROM}"
- label: "Create dedicated repo: ${GITHUB_ACCOUNT}/asciinema-recordings"
description: "Separate repository for all recordings"
- label: "Enter different repository"
description: "Specify another repository (user/repo format)"
If no current repo detected:
AskUserQuestion:
question: "Enter the GitHub repository URL for storing recordings:"
header: "Repository URL"
options:
- label: "Create dedicated repo: ${GITHUB_ACCOUNT}/asciinema-recordings"
description: "Separate repository for all recordings (Recommended)"
- label: "Enter repository manually"
description: "SSH (git@github.com:user/repo.git), HTTPS, or shorthand (user/repo)"
URL Normalization (handles multiple formats):
/usr/bin/env bash << 'NORMALIZE_URL_EOF'
# Normalize to SSH format for consistent handling
normalize_repo_url() {
local url="$1"
# Shorthand: user/repo -> git@github.com:user/repo.git
if [[ "$url" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$ ]]; then
echo "git@github.com:${url}.git"
# HTTPS: https://github.com/user/repo -> git@github.com:user/repo.git
elif [[ "$url" =~ ^https://github\.com/([^/]+)/([^/]+)/?$ ]]; then
echo "git@github.com:${BASH_REMATCH[1]}/${BASH_REMATCH[2]%.git}.git"
# Already SSH format
else
echo "$url"
fi
}
URL="${1:?Usage: provide URL to normalize}"
normalize_repo_url "$URL"
NORMALIZE_URL_EOF
Confirmation for free-form input (if user selected "Enter different/manually"):
AskUserQuestion:
question: "You entered '${USER_INPUT}'. Normalized to: ${NORMALIZED_URL}. Is this correct?"
header: "Confirm Repository"
options:
- label: "Yes, use ${NORMALIZED_URL}"
description: "Proceed with this repository"
- label: "No, let me re-enter"
description: "Go back to repository selection"
2.2 Recording Directory
AskUserQuestion:
question: "Where should recordings be stored locally?"
header: "Recording Directory"
options:
- label: "~/asciinema_recordings/${RESOLVED_REPO_NAME} (Recommended)"
description: "Example: ~/asciinema_recordings/alpha-forge"
- label: "Custom path"
description: "Enter a different directory path"
Note: ${RESOLVED_REPO_NAME} is the actual repo name from Phase 1.5 or Phase 2.1, not a variable placeholder. Display the concrete path to user.
2.3 Branch Name
AskUserQuestion:
question: "What should the orphan branch be named?"
header: "Branch Name"
options:
- label: "asciinema-recordings (Recommended)"
description: "Matches ~/asciinema_recordings/ parent directory pattern"
- label: "gh-recordings"
description: "GitHub-prefixed alternative (gh = GitHub storage)"
- label: "recordings"
description: "Minimal name"
- label: "Custom"
description: "Enter a custom branch name"
Naming Convention: The default asciinema-recordings matches the parent directory ~/asciinema_recordings/ for consistency.
Phase 3: Advanced Configuration
Purpose: Allow customization of compression and behavior parameters.
Configuration Parameters
Parameter
Default
Options
Idle threshold
30s
15s, 30s (Recommended), 60s, Custom (5-300)
zstd level
3
1 (fast), 3 (Recommended), 6, Custom (1-22)
Brotli level
9
6, 9 (Recommended), 11, Custom (1-11)
Auto-push
Yes
Yes (Recommended), No
Poll interval
5s
2s, 5s (Recommended), 10s
AskUserQuestion Sequence
3.1 Idle Threshold:
AskUserQuestion:
question: "How long should the chunker wait before creating a chunk?"
header: "Idle Threshold"
options:
- label: "15 seconds"
description: "More frequent chunks, smaller files"
- label: "30 seconds (Recommended)"
description: "Balanced chunk size and frequency"
- label: "60 seconds"
description: "Larger chunks, less frequent uploads"
- label: "Custom (5-300 seconds)"
description: "Enter a custom threshold"
AskUserQuestion:
question: "Ready to test recording? This requires you to start asciinema in another terminal."
header: "Recording Test"
options:
- label: "Guide me through it (Recommended)"
description: "Step-by-step instructions"
- label: "Skip this test"
description: "I'll verify manually later"
- label: "I've already verified recording works"
description: "Mark as passed"
If "Guide me through it" selected, display:
╔════════════════════════════════════════════════════════════════╗
║ USER ACTION REQUIRED: Recording Test ║
╠════════════════════════════════════════════════════════════════╣
║ ║
║ In a NEW terminal, run: ║
║ ┌────────────────────────────────────────────────────────┐ ║
║ │ asciinema rec ~/asciinema_recordings/test_session.cast │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ Then type a few commands and exit with Ctrl+D ║
║ ║
║ Come back here when done. ║
╚════════════════════════════════════════════════════════════════╝
Then Claude autonomously validates the created file:
# Claude runs after user confirms:
[RUN] Checking test_session.cast exists... ✓
[RUN] Validating JSON header... ✓ {"version": 2, ...}
[RUN] Checking line count... ✓ 23 events recorded
Test 10: Chunker Live Test
AskUserQuestion:
question: "Ready to test live chunking? This requires running recording + chunker simultaneously."
header: "Chunker Test"
options:
- label: "Guide me (Recommended)"
description: "Two-terminal workflow instructions"
- label: "Skip - I trust the setup"
description: "Skip live test"
Cause: zstd chunks not concatenating properly (overlapping data).
Fix: Ensure idle-chunker uses last_chunk_pos to avoid overlap:
/usr/bin/env bash << 'PREFLIGHT_EOF_2'
# Check for overlaps - each chunk should be sequential
for f in chunks/*.zst; do
zstd -d "$f" -c | head -1
done
PREFLIGHT_EOF_2
Key Design Decisions
Decision
Rationale
zstd for streaming
Supports frame concatenation (brotli doesn't)
brotli for archival
Best compression ratio (~300x for .cast files)
Orphan branch
Complete isolation, can't pollute main history
Idle-based chunking
Semantic breakpoints, not mid-output splits
Shallow clone
Minimal disk usage, can't accidentally access main
30s idle threshold
Balances chunk frequency vs semantic completeness
Post-Change Checklist
After modifying this skill:
Orphan branch creation scripts use heredoc wrapper
All bash blocks compatible with zsh (no declare -A, no grep -P)