A themeable visual status engine for Claude Code, Codex, and iTerm2.
Watch your terminal transform in real-time as your agent works — colors shift, backgrounds swap, and tab titles update to show exactly where things stand. Ship with themes or build your own.
Codex on macOS/iTerm2, Windows Terminal/PowerShell, or WezTerm, from the repo you want to skin:
npx -y visualhud@latest
codex --yoloThat installs VisualHUD into the current Git repo only. It writes .codex/,
.visualhud/, and .agents/skills/visualhud-* in that repo; it does not install
a global Codex or Claude hook.
VisualHUD setup/update skills should run platform setup helpers themselves. For
example, when a Windows repo should use WezTerm for live colors/backgrounds, the
skill should install with --platform wezterm, run setup-wezterm.ps1, and
handle straightforward config merges instead of sending you manual follow-up
commands.
On macOS/iTerm2, if iTerm2 has not been configured for tab colors and pane background images yet, run this once after install, then quit and reopen iTerm2:
./.visualhud/setup-iterm2.shWhen developing from this source checkout, the equivalent local command is
./setup-iterm2.sh.
On Windows/WezTerm, install with the WezTerm renderer and run the setup helper once:
npx -y visualhud@latest install codex --platform wezterm
powershell -ExecutionPolicy Bypass -File ./.visualhud/setup-wezterm.ps1Pokemon is the polished default for clean consumer installs:
Install into a specific repo from anywhere:
npx -y visualhud@latest install codex --target /path/to/other-repoChoose TMNT during install:
npx -y visualhud@latest --theme tmntSwitch themes after install; the next hook picks it up without restarting Codex:
./.visualhud/visualhud theme list
./.visualhud/visualhud theme set pokemon
./.visualhud/visualhud theme set tmnt
./.visualhud/visualhud theme currentCalibrate a theme:
./.visualhud/visualhud theme calibrate tmnt
./.visualhud/visualhud theme calibrate tmnt --live --delay 1Development checkout install:
./visualhud install codex --target /path/to/other-repoThe installer writes:
/path/to/repo/
.codex/hooks.json
.codex/hooks/visualhud-codex.sh
.visualhud/
engine.sh
set_bg.py
setup-iterm2.sh
setup-wezterm.ps1
visualhud
wezterm/
themes/
.agents/skills/
visualhud-setup/
visualhud-update/
visualhud-theme/
visualhud-feedback/
Claude support is repo-local and separate from Codex. Claude projects wire
.claude/settings.json to .claude/hooks/visualhud-claude.sh; Codex projects
wire .codex/hooks.json to .codex/hooks/visualhud-codex.sh.
Windows Terminal/PowerShell uses a separate renderer track for tab title and
progress status. WezTerm is the Windows path for live VisualHUD backgrounds:
VisualHUD emits a visualhudState user var and the WezTerm Lua module applies
per-window background/color overrides. Dynamic background images remain
unsupported in Windows Terminal because it exposes background images through
profile settings, not a per-hook escape sequence.
scripts/release-npm.sh --dry-runThat command checks npm auth, runs the full test suite, and runs
npm publish --dry-run without publishing.
Publish only after the dry-run is clean and npm auth is active:
scripts/release-npm.sh --publishThe publish path repeats the full test gate, repeats the npm dry-run, publishes,
then verifies the exact package version on the registry. If npm whoami fails,
run npm login in your shell and retry.
First publish bootstrap: npm requires account authentication for the initial
package publish. If your npm account requires two-factor auth, the first
scripts/release-npm.sh --publish may ask for an OTP or npm web auth.
After the package exists on npm, configure npm Trusted Publishing from this repo with:
npm run release:trustThat workflow publishes on version tags with GitHub OIDC (id-token: write) and
does not use NPM_TOKEN. Normal future releases should be version-bump, test,
commit, tag, and push the v* tag; the workflow runs npm ci, npm test, and
npm publish --access public.
Themes are data packs. A normal new theme should not require editing
engine.sh.
Tell your agent to follow THEMES.md and keep the work under this shape:
themes/<name>/theme.json
themes/<name>/sprites/
Minimum workflow:
mkdir -p themes/<name>/sprites
$EDITOR themes/<name>/theme.json
./visualhud theme set <name>
./visualhud theme calibrate <name>
bash tests/test-theme-system.sh
bash tests/test-theme-calibration.shFor branded/character themes, add real source-backed sprite assets and a
contact-sheet proof. Do not ship generated placeholders as final art. If adding
a theme requires engine changes, treat that as a theme-system contract change:
write the failing test first, update the JSON contract deliberately, then run
the full proof set in THEMES.md.
Future work: a dedicated theme creator workflow should scaffold the JSON,
sprite folders, calibration sheet, and TODO checklist automatically. Until that
exists, THEMES.md is the source of truth for agents and humans creating
Batman, Sonic, Power Rangers, or any third-party theme.
Claude Code is wired separately through:
.claude/settings.json
.claude/hooks/visualhud-claude.sh
Start or restart Claude Code from this repo. The Claude adapter defaults to the Pokemon theme and preserves the existing SDLC/TDD hooks.
To use TMNT for this repo, run:
./visualhud theme set tmntThe next hook picks up the selected theme. To override only one Claude process:
VISUALHUD_THEME=tmnt claudeRun the contract tests before calling an install or theme change done:
bash tests/test-visualhud-cli.sh
bash tests/test-visualhud-install.sh
bash tests/test-theme-system.sh
bash tests/test-visualhud-skills.sh
bash tests/test-theme-calibration.sh
bash tests/test-codex-visualhud.sh
bash tests/test-claude-visualhud.sh
bash tests/test-cooking-status.shFor visual changes, also review a screenshot, generated contact sheet, or a calibration walk. Shell tests prove hook output and state transitions; they do not prove aesthetics by themselves.
Generate the full ordered calibration list when a theme has too many visual states to inspect ad hoc:
./visualhud theme calibrate tmnt
./visualhud theme calibrate tmnt --jsonFrom a real iTerm2 pane, walk the same cycle one step at a time and correct specific step numbers:
./visualhud theme calibrate tmnt --live --delay 1
./visualhud theme calibrate tmnt --live --pauseWindows Terminal/PowerShell is supported for Codex hook install, title updates,
and Windows Terminal progress status. The theme JSON contract is portable and
the runtime does not require jq; JSON handling is done by the bundled Node
helper under scripts/.
WezTerm is supported for Codex hook install, title updates, and live
background/color changes. The WezTerm renderer sends a visualhudState user var
to wezterm/visualhud.lua, which updates the current window with
window:set_config_overrides().
Windows Terminal background images are still static profile settings. Use the WezTerm renderer for live sprite changes on Windows.
Claude Code and Codex fire lifecycle hooks on prompt, tool use, permission request, and stop events. VisualHUD intercepts those hooks and drives the terminal through a progression of stages — each with its own colors, background image where supported, and title.
Hook fires → adapter normalizes event → counter increments → stage advances → terminal renderer updates
Each terminal session is isolated via ITERM_SESSION_ID, WT_SESSION, or the
hook payload session id, so multiple windows don't stomp each other.
Review work is a separate lifecycle state from done. If a Codex/Claude code
review or background verification task is still running, VisualHUD shows the
theme's review state and keeps done/Mew/Pizza Party reserved for when that
review task completes.
A theme is a directory with a config file and optional assets:
themes/
pokemon/
theme.json
sprites/
charmander.png
charmeleon.png
...
tmnt/
theme.json
sprites/
tmnt-leonardo.png
tmnt-michelangelo.png
...
custom/
theme.json
See THEMES.md for the complete contract and required tests.
{
"name": "Example Theme",
"progress_bar": ["A", "B", "C"],
"stages": [
{
"max": 2,
"sprite": "alpha",
"badge": "A",
"name": "Alpha",
"color_family": "blue",
"color": [10, 20, 30],
"shades": [[10, 20, 30], [20, 35, 55]]
},
{
"max": 5,
"sprite": "beta",
"badge": "B",
"name": "Beta",
"color_family": "orange",
"color": [40, 50, 60],
"shades": [[40, 50, 60], [65, 75, 85], [85, 95, 110]],
"shade_sprites": ["beta", "beta-warm", "beta-hot"]
},
{
"max": 999999,
"sprite": "gamma",
"badge": "C",
"name": "Gamma",
"color_family": "green",
"color": [70, 80, 90],
"shades": [[70, 80, 90], [90, 110, 120]]
}
],
"blocked": { "sprite": "blocked", "badge": "BLOCK", "name": "Blocked", "color": [80, 75, 95] },
"review": { "sprite": "review", "badge": "REV", "name": "Reviewing", "stage": 2, "color": [80, 120, 200] },
"done": {
"sprite": "done",
"badge": "DONE",
"name": "Done",
"stage": 3,
"color": [40, 100, 255]
},
"idle": { "sprite": "idle", "badge": "IDLE", "name": "Idle", "stage": 3, "color": [40, 100, 255] },
"error": { "sprite": "error", "badge": "ERROR", "name": "Error", "color": [255, 40, 40] },
"context_alerts": {
"warning": { "min_percent": 70, "badge": "WARN", "name": "Context High", "color": [255, 190, 40] },
"critical": { "min_percent": 85, "badge": "CRIT", "name": "Context Critical", "color": [255, 255, 255] }
}
}No images needed — just a smooth color gradient that acts as a visual progress bar: red (just started) through yellow/green (making progress) to blue (done). It's a progress bar, not a health bar — the color tracks how far along the task is, not how "healthy" anything is. Works out of the box.
We researched the space. Nothing does what VisualHUD does.
| Tool | Changes Terminal Appearance | Driven by Process State | Configurable Themes | Multi-Terminal |
|---|---|---|---|---|
| VisualHUD (this) | bg + tab color + images + title | Claude Code hooks | Yes (theme.json) | Goal |
| claude-code-iterm2-tab-status | Tab title emoji only | Claude Code hooks | No (3 hardcoded states) | No |
| aiterm | Profile switch | Directory-based (cd) | No | Yes (6 terminals) |
| classmethod.jp blog | Background color | Start/stop only | No (2 hardcoded states) | No |
| Zestful | Separate overlay | Claude Code hooks | No | N/A |
| C.H.U.D. | Separate Electron window | Claude Code hooks | No | N/A |
| ccstatusline | Statusline text | Claude Code API | Yes (text themes) | N/A |
| claude-hud | Statusline text | Claude Code API | No | N/A |
The gap: Existing tools either change appearance but not based on process state (aiterm, sshbg), or react to process state through overlays / statuslines / tab emoji — not the terminal itself. Nobody ships a configurable state machine that drives full terminal appearance through hook lifecycle events.
Complementary tools we could integrate with:
- ccstatusline / claude-hud — they handle the statusline, we handle the terminal chrome
- aiterm — they set a baseline profile per project, we modulate it during runtime
- agent-notify — they do audio/push notifications, we do visual
Terminal escape code references for multi-terminal support:
- iTerm2:
OSC 1337;SetColors, tab color viaOSC 6;1;bg, Python API for background images - Kitty:
kitten @ set-colors, background viakitten @ set-background-image - WezTerm: Lua
set_config_overridesfor colors and background - Ghostty:
SIGUSR2config reload for theme switching - tmux: status bar styling via
set-option
VisualHUD is repo-local and functional for Codex, Claude Code, and iTerm2:
Working:
- Tab color changes via iTerm2 escape sequences (
OSC 6;1;bgandSetColors=tab) with per-session isolation. - Background, selection, cursor, and muted UI surface colors update through
OSC 1337;SetColors. - Window/tab title, badge text, and
hudProgress/hudContextuser vars update from hook lifecycle state. - Background images update through the iTerm2 Python API (
LocalWriteOnlyProfile) for the active terminal session. - Windows Terminal/PowerShell installs for Codex and updates the tab title plus
OSC 9;4progress status. - WezTerm installs for Codex and updates title, right status, colors, and live background sprites through
OSC 1337;SetUserVarplus the bundled Lua module. PreToolUse,UserPromptSubmit,Notification,PermissionRequest,Stop,StopFailure,TaskCompleted, andSessionStartare mapped into the engine by repo-local adapters.- Theme stages use
color_familyplusshades, so a character can keep the same sprite while the terminal chrome advances through multiple color steps. - Pokemon and TMNT both ship source-backed sprite packs and theme JSON.
- Context/token pressure is an ambient overlay: warning and critical label the state while preserving the active stage color/sprite.
Current hooks:
.codex/hooks/visualhud-codex.shmaps Codex events intoengine.shand falls back to TMNT when no active theme file exists. Clean installs into other Codex repos write Pokemon as the active theme by default..claude/hooks/visualhud-claude.shmaps Claude Code events intoengine.shand defaults to Pokemon.- Both adapters set
VISUALHUD_REAPPLY_DELAY=0.12by default to reapply title/color after TUI repaint.
Known limitations:
- iTerm2 and WezTerm are the complete live-background renderers today; Windows Terminal/PowerShell has title/progress support but not dynamic background images.
- Background images are static only; sprite animation is parked until a terminal adapter supports it cleanly.
- Badge content is text/emoji only because iTerm2 does not support image badges.
- Snapshot/restore of the original terminal profile is still planned.
Tested and confirmed:
- Background image per-session isolation works via
ITERM_SESSION_IDUUID extraction. - GIF backgrounds do not animate in iTerm2; frame cycling works, but low-res source GIFs are not good enough for full-screen backgrounds.
- iTerm2 does not support images in badges or corners: badges are text/emoji only, status bar icons are tiny, and inline images scroll with text.
- Pokemon HOME sprites and source-backed TMNT panel crops work as static backgrounds.
Themes can offer both static and animated sprites. Two approaches, depending on terminal support:
Set an animated GIF directly as the background image. If the terminal animates it natively, this is zero-effort animation.
| Terminal | Animated GIF Background | Status |
|---|---|---|
| WezTerm | Natively supported | Confirmed working |
| iTerm2 | First frame only, no animation | Confirmed not supported (2026-03-22) |
| Kitty | Graphics protocol has frame support | Untested |
| Ghostty | PNG/JPEG only, no GIF | Not supported |
Test script: ./test-gif-background.sh
Extract GIF frames to individual PNGs, cycle them as background images via the Python API. Tested on iTerm2: animation is smooth, but source sprites are too low-res (48-133px) for 4K displays. Would need high-res source art (512px+) to look good.
- Charmander: 69 frames, 30ms intervals (~33 FPS) — smooth animation confirmed
- Source quality is the bottleneck, not the cycling mechanism
- No open-source high-res animated Pokemon sprites exist (all game sprites are pixel art < 133px)
- AI upscaling produces blurry results on pixel art at this scale
Test script: ./test-frame-cycling.sh
Status: Parked. The frame cycling mechanism works. Revisit when high-res animated source art is available or AI upscaling improves.
Animation is parked until a terminal adapter supports it cleanly. A future
theme extension should keep the current stage contract and add animation fields
beside "sprite" instead of replacing the base sprite.
{
"stages": [
{
"name": "Charmander",
"max": 2,
"sprite": "charmander",
"color": [255, 60, 60],
"badge": "FIRE",
"animated_sprite": "charmander-run"
}
]
}Engine support for animated_sprite is not implemented yet. Add a failing test
before making that contract real.
Theme stages can define multiple "shades" for the same character. The engine
chooses a shade from the current tool-call position inside that stage's range,
so the sprite stays stable while the terminal chrome visibly advances:
Michelangelo (max 5)
tool 3 -> [255, 125, 25]
tool 4 -> [255, 150, 48]
tool 5 -> [255, 175, 70]
This is deliberate stage-local shading rather than cross-stage interpolation.
Stages can also define "shade_sprites" with one sprite per shade when the art
should advance with the color. TMNT uses this for character-color variants, and
future themes such as Batman and Power Rangers should use the same
JSON-plus-sprites contract instead of changing engine.sh.
Task completion (stage progression) is the main visual driver — background, tab color, images.
Token/context usage is a separate ambient indicator that doesn't interfere with the main visual:
- Badge/title suffix surfaces only when it matters (
CTX 70%+warning,CTX 85%+critical) - The task stage color, cursor, and sprite stay intact; context labels must not gray-wash or emergency-wash the pane
- Themes can name those alerts: Pokemon uses Pokemon Center/Chansey and Nurse Joy/Blissey, TMNT uses Casey Jones for critical context
- Codex can derive context percent from hook payload token data or a matching session JSONL token-count event
These are independent axes: you can be on stage 3 (Charizard) with low token usage, or stage 1 (Charmander) with high token usage.
The TMNT skin is only visually complete once real source images exist on disk. Chat attachments are not repo files, so place a four-panel character-select reference at:
assets/source/tmnt/character-select.png
Then generate source-backed sprite assets:
python3 scripts/import-tmnt-sprites.py \
--source assets/source/tmnt/character-select.png \
--output-dir themes/tmnt/sprites \
--source-label "tmnt-character-select-reference"
The importer writes tmnt-leonardo.png, tmnt-michelangelo.png,
tmnt-donatello.png, tmnt-raphael.png, and manifest.json with crop
provenance. Do not ship generated placeholder TMNT art; source-backed PNGs must
include the manifest.
For character-focused one-off assets, the importer strips neutral corner mattes
from source images and renders transparent areas over the matching theme color.
The generated manifest records that color as backdrop_color, which prevents
yellow April, white Casey, or other stage families from drifting back to gray.
Documenting these so we don't repeat them:
-
ITERM_SESSION_IDformat mismatch — The env var isw0t0p0:UUIDbut the iTerm2 Python API'ssession.session_idis justUUID. Must strip the prefix:sid.split(":")[-1]. This caused background images to silently never match any session. -
iTerm2 Python API connection — Requires "Enable Python API" in iTerm2 Preferences > General > Magic. Without it, all Python API calls fail silently if errors are suppressed.
-
Badge positioning —
badge_top_marginis pixel-based, which breaks on different window sizes. Fix: dynamically calculate from window height on each stage change (rows * cell_height * 0.85). -
Cross-window contamination — Original implementation used TTY device for session isolation, which doesn't work in hook sandbox context (
psis blocked,ttyreturns garbage). Fix: useITERM_SESSION_IDenv var which is always inherited by child processes. -
Badge disappears on resize/different monitors —
badge_top_marginis pixel-based (not percentage). When the window is shorter than the margin value, the badge goes off-screen. Moving windows between monitors with different sizes makes it worse. Dynamic recalculation inset_bg.pyhelps but only fires on stage transitions, not on resize. Needs a resize listener in v0.1.
Pre-release. Repo-local Codex and Claude Code hook wiring works on iTerm2. Windows Terminal/PowerShell Codex installs work for title and progress status. WezTerm Codex installs work for title, status, colors, and live backgrounds on Windows.
Deep-dived every project above and pulled the best patterns:
| Idea | Stolen From | Why It's Good |
|---|---|---|
| Snapshot/restore terminal state on exit | JasperSui | Clean teardown — save tab color, title, badge before we touch anything, restore when Claude stops |
"Needs attention" state via Notification hook |
JasperSui | We only use PreToolUse/Stop — missing the "waiting for permission" state entirely |
| Stale PID cleanup for orphaned state files | JasperSui | Dead sessions shouldn't leave ghost state files around |
| Auto-contrast color adjustment | JasperSui | If theme color is too close to tab's existing color, auto-pick a visible alternative |
| Terminal abstraction layer (per-terminal module) | aiterm | Each terminal (iTerm2, Kitty, Ghostty, WezTerm) gets its own adapter with same interface |
| Context health thresholds (green <70%, yellow 70-84%, red 85%+) | ccstatusline | Modulate visual intensity based on context window usage |
| DI pattern for testing (no heavy mocks) | claude-hud | Inject deps, test cleanly |
| Config priority: env vars > config file > defaults | JasperSui | Standard, flexible, zero-surprise |
| Hot-reload config on file mtime change | JasperSui | Change theme without restarting Claude |
| Focus-to-dismiss attention state | JasperSui | Attention clears when user looks at the tab |
| Multi-agent priority display (highest alert wins) | Zestful | When running multiple sessions, show the most urgent state |
Core engine + themeable + works reliably:
Done:
- Per-session isolation via
ITERM_SESSION_ID(UUID extraction) - Background images matched to correct session (fixed
w0t0p0:UUID→UUID) - Badge emoji per-stage (🔥⚡🌿💧) with text progress bar at top-right
- Badge position fixed: top-right with zero margins (no more disappearing on resize)
-
setup-iterm2.sh— automated iTerm2 settings configuration - Cursor color uses iTerm2-native
SetColors=curbg=instead of unsupportedOSC 12 - Pokemon HOME sprites (512x512) — crisp on 4K
- 11 stages with complete evolution lines (Raichu added)
- Demo script (
demo.sh) for testing all features - Confirmed: GIF backgrounds don't animate in iTerm2, frame cycling works but needs hi-res source art
- Confirmed: iTerm2 badges are text/emoji only, no images
Current focus — Harden the theme engine:
The theme engine is now data-driven: engine.sh reads theme.json, advances
stage counters, applies stage shades, resolves sprite paths, and drives iTerm2.
Pokemon and TMNT live under themes/, and both Codex and Claude use repo-local
adapters.
Next hardening work:
- Replace any weak TMNT one-off cover-art states with character-focused source art.
- Add the next branded theme candidates, Batman and Power Rangers, as JSON/sprite packs that prove third-theme parity without engine changes.
- Add a terminal-renderer abstraction for Windows Terminal/PowerShell, Kitty, WezTerm, and Ghostty.
- Add snapshot/restore so VisualHUD can return terminal chrome to its original state.
- Add a CLI installer/doctor once the repo-local contract is stable.
Then — Polish:
- Window border integration (JankyBorders) — colored border around iTerm2 window that changes with stage progression:
- Uses JankyBorders (
brew install borders) withapply-to=<window-id>for per-window support - Border color matches current stage (red → yellow → green → blue)
- Border thickness increases as task progresses (thin at start, thick at done)
- Done state: thick border stays in final color as persistent visual indicator
- Supports multiple iTerm2 windows simultaneously (each gets its own border color based on its session state)
- Replaces any current janky border solution entirely
- Get window ID via macOS Accessibility API or yabai, pass to
borders apply-to=<wid> active_color=0xffRRGGBB width=N - Supports gradient (
gradient(top_left=...,bottom_right=...)) and glow (glow(0xAARRGGBB)) effects - Requires macOS 14.0+ (Sonoma)
- Uses JankyBorders (
- Snapshot/restore — save terminal state before first hook, restore on Stop
- "Needs attention" state — hook into
Notificationevent forpermission_prompt - Stale state cleanup — auto-remove state files for dead sessions
- Config priority — env vars >
~/.visualhud/config.json> defaults
What makes it a real tool people can actually use:
- CLI:
visualhud install,visualhud theme list,visualhud theme set <name> - CLI:
visualhud theme preview— cycle through stages in current terminal - CLI:
visualhud doctor— validate setup (terminal, hooks, theme, permissions) - Auto-configure Claude Code hooks in
settings.json - Validate theme configs on install
- Uninstall / reset terminal to defaults
- Support custom stage counts (some tasks are 5 tool calls, some are 50)
- Hot-reload config — change theme.json mid-session, picks up on next hook
- Auto-contrast — if theme color clashes with existing tab color, auto-adjust
- Terminal abstraction — iTerm2 adapter first, then Kitty/WezTerm/Ghostty adapters
- Context health modulation — intensify visuals as context window fills (70%/85% thresholds)
- macOS notifications + sound — optional
afplayon stage transitions or attention - Plugin marketplace distribution (Claude Code
/plugin install) - README with install instructions, screenshots, GIFs
- Pre-built theme packs (Nord, Dracula, Gruvbox color palettes as themes)
Things that would be sick but aren't urgent:
- Theme builder CLI — interactive wizard to define stages, pick colors, assign images
- Community themes — repo of user-contributed themes,
visualhud theme install <github-url> - Sound effects — play a short sound on stage transitions (evolution sound, anyone?)
- Adaptive stage count — auto-scale stages based on historical tool-call patterns per project
- Multi-tool awareness — different visuals for Read vs Write vs Bash vs Agent
- Multi-agent priority — when multiple Claude sessions run, surfaces the most urgent state
- Focus-to-dismiss — attention state auto-clears when user focuses the tab (iTerm2 FocusMonitor API)
- Team themes — shared theme configs via dotfiles repo
- Image generation — AI-generate theme assets from a prompt ("medieval RPG progression")
- Stats dashboard — track cooking sessions, average tool calls, time per stage
- tmux support — status bar integration for tmux users
- Unix socket IPC — replace file-based state with socket for sub-millisecond updates
- iOS companion — push notifications + Dynamic Island for mobile awareness
- Context-aware themes — auto-switch theme based on project type (production = red safety theme)
- OSC 8 hyperlinks — clickable links in terminal output
- ModelIntelligence score — visual indicator of estimated quality as context fills
