by s0xDk
Ghostty Blackhole puts a real, ray-traced black hole inside your terminal. It grows as Claude Code's context window fills up, live. A fresh session is a quiet hole in the corner. A full one swallows half your screen. You'll always see /compact coming.
# Add to your Claude Code skills
git clone https://github.com/s0xDk/ghostty-blackholeGuides for using cli tools skills like ghostty-blackhole.
Last scanned: 6/12/2026
{
"issues": [],
"status": "PASSED",
"scannedAt": "2026-06-12T08:25:44.011Z",
"npmAuditRan": true,
"pipAuditRan": true,
"promptInjectionRan": true
}No comments yet. Be the first to share your thoughts!
Top skills in this category by stars
30 days in the Featured rail · terms & refunds

Landing page: s13k.dev/blackhole
A black hole floating inside your Ghostty terminal. It starts small and grows to swallow your screen, driven by whichever size mode you pick: a built-in pomodoro clock (grow through the hour, demand a break, leave you alone once you take it), or token mode, where it tracks how full Claude Code's context window is in real time.
Modeled on Eric Bruneton's black hole shader,
which beam-traces Schwarzschild geodesics against precomputed lookup tables.
A Ghostty custom shader is a single Shadertoy-style fragment pass with no
custom textures, so the lookup tables are replaced by doing the physics live:
every pixel near the hole integrates its own null geodesic through the
Schwarzschild metric (the Binet-form acceleration a = -3/2 h² x/r⁵, which
gives the exact photon bending). Your terminal contents play the role of the
lensed background sky. Nothing below is painted on — it all falls out of the
ray tracing:
b_crit = (3√3/2) r_s
spiral through the horizon and come back black. Text near the edge is
stretched into the photon ring before it disappears; text behind the hole
really is gone.α = 2r_s/b), so only pixels near
the hole pay for the integration. Blue bends slightly more than red out
there for a touch of chromatic aberration.g = √(1 − 1.5 r_s/r)/(1 − β·k̂) — the approaching side is blue-hot and
boosted by g^N, the receding side dim and red.1.5 r_s photon sphere pick up
every disk crossing; the bright thin ring is emergent, not drawn.STAR_GAIN to enable).r advances
at the proper rate √(1 − 1.5 r_s/r), so the inner orbits visibly freeze;
the whole disk also winds down as the hole grows heavier (DILATION_MIN).WORK_AREA fraction (your prompt) is never
distorted. Drift speed and reach follow its size: small and calm, big
and restless.The break reminder is computed entirely inside the shader — no daemon, no
shell hooks, nothing outside blackhole.glsl.
Shaders are stateless (no buffers persist between frames, and Ghostty has no
custom uniforms), so a shader cannot remember when your work streak began.
Instead the schedule is anchored to the wall clock via iDate:
WORK_PERIOD_MIN (default 55 min), collapses
back to small in the last minute, and stays small through BREAK_MIN
(default 5 min). With 55+5 the peak hits at five-to-the-hour — a fixed,
predictable rhythm.iTimeCursorChange tracks cursor activity in your
terminal. Stop using it for IDLE_FADE_SEC (default 90 s) and the hole
shrinks live, gone entirely after a few minutes of quiet — it never nags
while you aren't actually working.The trade-off of self-containment: the cycle won't re-anchor to a break you take at an odd time — it's an hourly bell, not a per-streak stopwatch.
Caveat: stock Ghostty (through 1.3) declares
iDatebut never populates it — it is always zero — so on current releases the wall-clock schedule doesn't advance: the hole sits at its small cycle-floor size and only the typing detector works. The full cycle comes alive once Ghostty wiresiDateup (you can preview it today withTIME_SCALE, which runs offiTimeinstead). Token mode, the default, is unaffected.
What drives the hole is selected by SIZE_MODE near the top of blackhole.glsl:
MODE_POMODORO — the self-contained wall-clock schedule described above. Works
standalone, no setup beyond the shader (but see the iDate caveat above).MODE_TOKENS (default) — the hole tracks Claude Code's context-window
fill. Requires the bundled command (below).MODE_DEMO — a self-running 42-second showcase loop for recording demos:
the hole grows from the corner seed to 100 % exactly as token mode would,
while the disk look tours the tuner presets (Inferno → Gargantua → M87* donut
→ Face-on ember → Quasar → Blazar → Pure lens → Inferno), crossfading at each
~5 s slot boundary. Everything runs off iTime inside one compiled shader —
no reloads, so a recording never hitches. Toggle it with ./demo-mode.sh on|off (which also reloads Ghostty); the cursor channel is ignored in demo
mode, so a live Claude session can't disturb a recording. Record any full
cycle — the loop restart is obvious (the hole snaps back to the corner
seed).The hole reflects how full Claude's context window is, live:
TOKEN_AREA_MIN (default 0.06 %) of the terminal area — the same
felt size on any window shape.TOKEN_AREA_MAX (default ~3 % of the
terminal at 100 % context — that's the shadow; the bright disk reaches
~3× past it, so it reads far bigger), moves faster, and its allowed roam box
expands out of the corner — left and down — until it covers the whole
playable screen above the work area; the hole wanders pseudo-randomly
through all of it. The orbit is scaled to the box (never clipped —
clipping would park it dead at the boundary), with margins that keep the
shadow and bright inner disk on screen while it's small./compact or a new session — snaps back to the corner seed.Ghostty custom shaders take no custom uniforms — but they do get the cursor
color (iCurrentCursorColor), and any program can set the cursor color with a
standard OSC 12 escape. So the token count rides in on the cursor: a single
bundled script, claude-token.py, is wired into Claude Code three ways and
encodes the context fill into the low nibbles of an amber cursor color
(#f5b000 empty → #f0bf0a full); the shader decodes it back out every
frame. The fixed high nibbles plus a 4-bit checksum form a 16-bit signature,
so a theme's own cursor color can't accidentally summon a black hole. No file
is rewritten, nothing reloads, there is no recompile hitch — updates land on
the next frame.
Level steps land smoothly, too: Ghostty bumps iTimeCursorChange on any
cursor change including color and snapshots the old color into
iPreviousCursorColor, so the shader decodes both and glides between the two
levels — discrete updates read as continuous motion instead of popping the
whole warp field. The glide time scales with the jump (TOKEN_GLIDE_*):
1 % ticks ease over 0.3 s, a 10 % jump takes 1 s, capped at 1.5 s.
| Wiring | When it fires | What it does |
|---|---|---|
statusLine |
every assistant turn | encodes the context fill (0..1, 1/250 steps) into the cursor color and prints a built-in-style line: ⚫️ ██████░░░░ 61% · Fable 5 · Projects/blackhole · ⎇ main · $1.27 · 5h 24% · wk 41% (the last segment is your Claude usage limits; a window past 80 % also shows its reset time) |
SessionStart hook |
session start / resume / /clear |
resets to the corner seed (0.0) |
SessionEnd hook |
session exit (/exit, ctrl-d, …) |
resets the cursor color (OSC 112) — no signature means no hole |
A cursor color without the signature means "no session", so a bare install
shows nothing until a live session encodes a real fill. (The shader's
TOKEN_LEVEL define remains as a manual override for hand-testing a size;
it only applies while the cursor carries no signal.)
Two pieces — the shader and the command:
Point Ghostty at the shader (see Install below) with
SIZE_MODE MODE_TOKENS (the default).
Add this to ~/.claude/settings.json (adjust the path), then start a new
Claude Code session:
{
"statusLine": {
"type": "command",
"command": "/path/to/blackhole_ghostty/claude-token.py"
},
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "/path/to/blackhole_ghostty/claude-token.py" }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "/path/to/blackhole_ghostty/claude-token.py" }] }]
}
}
This is global, so the hole reacts to any Claude Code session. A few notes: