A thinking screen for terminal AI agents: an event protocol with pluggable adapters, and a shoreline that rises while your agent works.
HTML
5
432 commits
updated Sep 22, 2026
Waiting for an agent is dead time. You fire off a prompt, the terminal goes quiet, you tab away, and then you forget to come back. xscapes gives that dead time a face: a small living shoreline that runs beside your agent, rises while it works, and knocks when it wants you.

It is two things. Underneath there is an event protocol with pluggable adapters, which is the part that generalises to any agent. On top there is a reference scape, which is what that protocol looks like when you give it a sea and a crab.
xscapes claude
That is the whole thing. Claude Code in the top of your window, the shoreline underneath it, one command and no tmux.
xscapes claude -beside gives you the older side-by-side layout instead: the
agent in its own tmux pane, the scape in the next one.
One command, on a Mac or Linux, with or without Go. Windows is not supported natively: the host runs the agent on a Unix pseudo-terminal. Inside WSL it is a Linux box and should work as one; nobody has tested it yet.
curl -fsSL https://donlucasx.github.io/xscapes/install.sh | sh
Then wire it to your agent and run the agent inside the scape:
xscapes install claude --apply && xscapes claude # Claude Code
xscapes install kimi --apply && xscapes kimi # Kimi Code CLI
xscapes install hermes --apply && xscapes hermes # Hermes Agent
xscapes inside <any command> # anything else, no hooks
xscapes install <agent> without --apply prints the plan and writes nothing;
with it, the hooks go in after a backup, and uninstall takes out exactly
what was written. The launcher refuses to start without the agent's hooks,
or with only part of them (an older install, or a block trimmed by hand),
and prints that install line instead; hooks written by hand count as
installed. xscapes <agent> -watch=on runs on the output watcher without
them, which sees work and prompts but no tool names, asks or sub-agents;
xscapes inside <agent> skips the check and binds to whatever hooks fire.
The script puts the release binary in ~/.local/bin. If that directory is
not on your PATH it adds one line to your shell's rc file for every new
terminal, and prints the next commands with a path that works in the
current one. It is short; read it first if you like:
site/install.sh.
With Go you can build it yourself instead. go install on its own puts the
binary in ~/go/bin, which is on no Mac's PATH by default, so send it
somewhere that is:
GOBIN=~/.local/bin go install github.com/donlucasx/xscapes@latest
Then run xscapes claude from whatever project you are working in. It is not
something you run from this repo.
xscapes claude has no dependencies at all. The older side by side layout,
xscapes claude -beside, wants tmux:
brew install tmux
Without tmux it opens a second Terminal window instead. Uninstall restores your settings byte for byte.
xscapes is not a terminal emulator, and deliberately so. It runs the agent on a pty, tells it the window is only as tall as its own band, and pins that band to the top of the screen with a scroll region. The agent's bytes then reach your terminal untouched -- so no bug in xscapes can corrupt the agent's own display, which is the failure a real emulator invites.
The band is anchored at the top because that is the only place it can be. Lines scrolled out of a scroll region reach your scrollback only when the region starts at row 1: measured in Terminal.app, a region on rows 1-10 keeps every scrolled line and a region on rows 5-14 keeps none. Anything painted above the agent would cost you the ability to scroll back through its output. So the scape reads downward instead -- a strip of sky under the agent, then the sea, then the beach -- and a taller window spends its extra rows on beach, where the agent's work is written.
It runs on the alternate screen, the way vim or htop do. That is not cosmetic: growing a window makes the terminal pull scrolled-off lines back in from history, which pushes the agent's UI down and out of its band -- and the agent never notices, because it emits nothing at all on a resize and places its input purely by relative moves from wherever the cursor is. Measured both ways: plain Claude Code survives that resize, Claude Code in a band on the main screen does not. The alternate screen has no history, so there is nothing to pull back.
The alternate screen has no scrollback of its own, so xscapes feeds the
terminal's. Every row that scrolls out of the agent's band is written into the
main buffer behind the alternate screen, through a buffer switch that clears
nothing, and Terminal.app shows the main buffer above the alternate screen: scroll
up and the transcript is there, in order, right under the command you typed,
with the wheel, selection and search you already use. Terminal.app drops most of
that history when the alternate screen is given back, so when the session ends
the transcript is printed once more, followed by the agent's final screen, the
way a plain session leaves it. Early in a session there are blank rows between
the transcript and the band until enough output has filled the buffer.
-history=false turns all of it off.
-alt=false runs on the main screen instead and takes the resize problem back.
What xscapes gives up by not being an emulator: the sea does not show through the agent's own blank space. Its band is its own.
Everything on screen means one thing, and no two things share a channel.
| what | how it reads |
|---|---|
| how hard the agent is working | the sea: how many swells are travelling, how tall, whitecaps above half |
| what it is doing right now | writing in the sand, newest brightest, older lines taken by the tide |
| something is broken | the companion: claws down, stalks short, amber eyes, and it stays until you come back |
| it needs you | a solid balloon in a warm colour, plus a bright chime |
| it finished | a dotted balloon in a cool colour, plus a low sonar note |
| subagents | crablets, one per agent, some of them already in the water |
| context left | the sun by day, the moon by night -- one body, phase and height, with a readout that stays quiet until 40% used |
| time of day | the sky, from your actual clock |
The rule underneath is that the water is the work and the sky is the world. Sea state always means the agent. Sky, light and time always mean reality. Nothing crosses. It is the reason a glance tells you anything at all: you never have to ask which of two meanings a change is carrying.
The two knocks are deliberately different in shape as well as colour, so they survive a screenshot and a colourblind reading, and they carry different sounds, so they survive you looking at another pane. "I finished" and "I am blocked on you" are not the same message and should never look the same. A third sound, and only a third, marks a finish that left something broken: the companion is still worried when the turn ends, so the done knock plays its worried voice. Nothing else makes a sound.
Adapters translate an agent into events. The engine folds events into a scene. Anything that can emit these can drive a scape.
session_start prompt tool_start tool_end error test_pass test_fail
sub_start sub_end needs_input done compact context todo session_end
Send one by hand:
xscapes emit tool_end -tool Read -target internal/auth/handler.go -detail "142 lines"
xscapes emit needs_input -text "allow Bash?"
Events reach a running scape over a unix socket in ~/.config/xscapes/run/,
and spool to a file when nothing is listening, so a scape started late still
picks up the session. Settings are read from XSCAPES_*; the pre-rename
ASCIISCAPES_* names still work and say so on stderr.
Adapter 1: Claude Code, via hooks. xscapes install claude writes them.
The payload schema was read out of the Claude Code binary rather than guessed;
see notes/claude-hooks-verified.md.
Adapter 2: any program, with no hooks at all. xscapes inside <command>
watches the program's own traffic: its output is work, Enter is a prompt, and
quiet after work is done (the "prompt back" nudge, with the done cue). It
cannot know a question from a finish, so the ask cue never rings from it, and
it names no tools; the sand carries the last line the program printed. It is
on by default until an agent's hooks announce a session, and -watch=on|off
forces it either way.
Adapter 3: Kimi Code CLI, via its shell hooks. xscapes install kimi
appends the hooks to ~/.kimi-code/config.toml as a marked block, and
xscapes kimi runs Kimi inside the scape. Kimi's event names are Claude
Code's, but its payloads differ where it matters, and each was measured on
Kimi itself (2026-09-17) rather than assumed: the prompt arrives as parts and the error
as an object (both read); its todo tool is TodoList (it lights the stars);
its permission prompt is the ask and its answer takes the ask down; Esc
sends an interrupt in place of a finish (the scene settles, no cue); a
subagent's own finish carries no mark, so a finish while one is open is not
the turn's; and the spend counter and the moon read Kimi's own session
files, since its hooks name no transcript.
Adapter 4: Hermes Agent, via its shell hooks. xscapes install hermes
merges them into ~/.hermes/config.yaml. Hermes asks for consent the first
time each hook fires; hermes hooks doctor answers once for all of them.
Every installer prints a plan and writes only with --apply; every
uninstall removes exactly what it wrote. Writing another adapter means
emitting the events above. Nothing in the engine knows what any agent is.
xscapes companion crab # choose the animal: crab (default) or cat
xscapes scape shore # choose the scape: vista (default) or shore; a running scape switches
xscapes inside <command> # host any command inside the scape, not just claude
xscapes install kimi # Kimi Code CLI hooks (a plan; --apply writes)
xscapes kimi # Kimi Code CLI inside the scape
xscapes install hermes # Hermes Agent hooks (a plan; --apply writes)
xscapes hermes # Hermes Agent inside the scape
xscapes claude -beside # the older side by side layout, in tmux
xscapes claude -scape 24 # give the shoreline more rows (default: two fifths)
xscapes claude -fps 8 # slow the scape down
xscapes claude -history=false # do not mirror the transcript into the terminal's scrollback
xscapes claude -alt=false # run on the main screen, at the cost below
xscapes -live # the scape in this terminal, Ctrl-C to quit
xscapes -info # colour profile, size, which sound player
xscapes notify # hear the three knocks
xscapes replay session.jsonl # feed a recorded session back through the engine
XSCAPES_COMPANION=cat … # override the companion for one run, without saving it
XSCAPES_SCAPE=vista … # override the scape for one run, without saving it
XSCAPES_SILENT=1 … # mute
XSCAPES_HOOKLOG=file … # the hook command appends every raw payload and where it went (adapter debugging)
XSCAPES_EVENTLOG=file … # the scape appends every event its reducer applied (the other end of the same question)
XSCAPES_HOOKLOG=1 … # =1 picks ~/.config/xscapes/hooks.jsonl (and events.jsonl), like XSCAPES_TRACE=1
# ⚠ the hook log holds every payload verbatim: your prompts, commands and paths
xscapes claude -print shows you how the window will be split and what will be
run in it, and changes nothing. xscapes claude -beside -print does the same for
the tmux layout.
It is Go, standard library only, one static binary. No Node, no Python, no framework.
Three things in here were harder than they look and are commented where they live:
XSCAPES_CHROMA tunes it.Run the tests with go test ./.... The interesting ones assert things that
looked fine on screen and were not: that whiskers touch fur on their own row,
that a sixty second permission nag rings once rather than once a minute, and
that a killed agent eventually settles instead of leaving the companion working
forever.
Early. The scape runs, the Claude Code adapter works, the sounds work, and the launcher works. Stars for completed todos are specified and not built. The companion's final coat and markings are still being chosen.
MIT.
710 followers · starred Sep 2026
HTML
83.1%
Go
16.4%
A thinking screen for terminal AI agents: an event protocol with pluggable adapters, and a shoreline that rises while your agent works.
HTML
5
432 commits
updated Sep 22, 2026
Waiting for an agent is dead time. You fire off a prompt, the terminal goes quiet, you tab away, and then you forget to come back. xscapes gives that dead time a face: a small living shoreline that runs beside your agent, rises while it works, and knocks when it wants you.

It is two things. Underneath there is an event protocol with pluggable adapters, which is the part that generalises to any agent. On top there is a reference scape, which is what that protocol looks like when you give it a sea and a crab.
xscapes claude
That is the whole thing. Claude Code in the top of your window, the shoreline underneath it, one command and no tmux.
xscapes claude -beside gives you the older side-by-side layout instead: the
agent in its own tmux pane, the scape in the next one.
One command, on a Mac or Linux, with or without Go. Windows is not supported natively: the host runs the agent on a Unix pseudo-terminal. Inside WSL it is a Linux box and should work as one; nobody has tested it yet.
curl -fsSL https://donlucasx.github.io/xscapes/install.sh | sh
Then wire it to your agent and run the agent inside the scape:
xscapes install claude --apply && xscapes claude # Claude Code
xscapes install kimi --apply && xscapes kimi # Kimi Code CLI
xscapes install hermes --apply && xscapes hermes # Hermes Agent
xscapes inside <any command> # anything else, no hooks
xscapes install <agent> without --apply prints the plan and writes nothing;
with it, the hooks go in after a backup, and uninstall takes out exactly
what was written. The launcher refuses to start without the agent's hooks,
or with only part of them (an older install, or a block trimmed by hand),
and prints that install line instead; hooks written by hand count as
installed. xscapes <agent> -watch=on runs on the output watcher without
them, which sees work and prompts but no tool names, asks or sub-agents;
xscapes inside <agent> skips the check and binds to whatever hooks fire.
The script puts the release binary in ~/.local/bin. If that directory is
not on your PATH it adds one line to your shell's rc file for every new
terminal, and prints the next commands with a path that works in the
current one. It is short; read it first if you like:
site/install.sh.
With Go you can build it yourself instead. go install on its own puts the
binary in ~/go/bin, which is on no Mac's PATH by default, so send it
somewhere that is:
GOBIN=~/.local/bin go install github.com/donlucasx/xscapes@latest
Then run xscapes claude from whatever project you are working in. It is not
something you run from this repo.
xscapes claude has no dependencies at all. The older side by side layout,
xscapes claude -beside, wants tmux:
brew install tmux
Without tmux it opens a second Terminal window instead. Uninstall restores your settings byte for byte.
xscapes is not a terminal emulator, and deliberately so. It runs the agent on a pty, tells it the window is only as tall as its own band, and pins that band to the top of the screen with a scroll region. The agent's bytes then reach your terminal untouched -- so no bug in xscapes can corrupt the agent's own display, which is the failure a real emulator invites.
The band is anchored at the top because that is the only place it can be. Lines scrolled out of a scroll region reach your scrollback only when the region starts at row 1: measured in Terminal.app, a region on rows 1-10 keeps every scrolled line and a region on rows 5-14 keeps none. Anything painted above the agent would cost you the ability to scroll back through its output. So the scape reads downward instead -- a strip of sky under the agent, then the sea, then the beach -- and a taller window spends its extra rows on beach, where the agent's work is written.
It runs on the alternate screen, the way vim or htop do. That is not cosmetic: growing a window makes the terminal pull scrolled-off lines back in from history, which pushes the agent's UI down and out of its band -- and the agent never notices, because it emits nothing at all on a resize and places its input purely by relative moves from wherever the cursor is. Measured both ways: plain Claude Code survives that resize, Claude Code in a band on the main screen does not. The alternate screen has no history, so there is nothing to pull back.
The alternate screen has no scrollback of its own, so xscapes feeds the
terminal's. Every row that scrolls out of the agent's band is written into the
main buffer behind the alternate screen, through a buffer switch that clears
nothing, and Terminal.app shows the main buffer above the alternate screen: scroll
up and the transcript is there, in order, right under the command you typed,
with the wheel, selection and search you already use. Terminal.app drops most of
that history when the alternate screen is given back, so when the session ends
the transcript is printed once more, followed by the agent's final screen, the
way a plain session leaves it. Early in a session there are blank rows between
the transcript and the band until enough output has filled the buffer.
-history=false turns all of it off.
-alt=false runs on the main screen instead and takes the resize problem back.
What xscapes gives up by not being an emulator: the sea does not show through the agent's own blank space. Its band is its own.
Everything on screen means one thing, and no two things share a channel.
| what | how it reads |
|---|---|
| how hard the agent is working | the sea: how many swells are travelling, how tall, whitecaps above half |
| what it is doing right now | writing in the sand, newest brightest, older lines taken by the tide |
| something is broken | the companion: claws down, stalks short, amber eyes, and it stays until you come back |
| it needs you | a solid balloon in a warm colour, plus a bright chime |
| it finished | a dotted balloon in a cool colour, plus a low sonar note |
| subagents | crablets, one per agent, some of them already in the water |
| context left | the sun by day, the moon by night -- one body, phase and height, with a readout that stays quiet until 40% used |
| time of day | the sky, from your actual clock |
The rule underneath is that the water is the work and the sky is the world. Sea state always means the agent. Sky, light and time always mean reality. Nothing crosses. It is the reason a glance tells you anything at all: you never have to ask which of two meanings a change is carrying.
The two knocks are deliberately different in shape as well as colour, so they survive a screenshot and a colourblind reading, and they carry different sounds, so they survive you looking at another pane. "I finished" and "I am blocked on you" are not the same message and should never look the same. A third sound, and only a third, marks a finish that left something broken: the companion is still worried when the turn ends, so the done knock plays its worried voice. Nothing else makes a sound.
Adapters translate an agent into events. The engine folds events into a scene. Anything that can emit these can drive a scape.
session_start prompt tool_start tool_end error test_pass test_fail
sub_start sub_end needs_input done compact context todo session_end
Send one by hand:
xscapes emit tool_end -tool Read -target internal/auth/handler.go -detail "142 lines"
xscapes emit needs_input -text "allow Bash?"
Events reach a running scape over a unix socket in ~/.config/xscapes/run/,
and spool to a file when nothing is listening, so a scape started late still
picks up the session. Settings are read from XSCAPES_*; the pre-rename
ASCIISCAPES_* names still work and say so on stderr.
Adapter 1: Claude Code, via hooks. xscapes install claude writes them.
The payload schema was read out of the Claude Code binary rather than guessed;
see notes/claude-hooks-verified.md.
Adapter 2: any program, with no hooks at all. xscapes inside <command>
watches the program's own traffic: its output is work, Enter is a prompt, and
quiet after work is done (the "prompt back" nudge, with the done cue). It
cannot know a question from a finish, so the ask cue never rings from it, and
it names no tools; the sand carries the last line the program printed. It is
on by default until an agent's hooks announce a session, and -watch=on|off
forces it either way.
Adapter 3: Kimi Code CLI, via its shell hooks. xscapes install kimi
appends the hooks to ~/.kimi-code/config.toml as a marked block, and
xscapes kimi runs Kimi inside the scape. Kimi's event names are Claude
Code's, but its payloads differ where it matters, and each was measured on
Kimi itself (2026-09-17) rather than assumed: the prompt arrives as parts and the error
as an object (both read); its todo tool is TodoList (it lights the stars);
its permission prompt is the ask and its answer takes the ask down; Esc
sends an interrupt in place of a finish (the scene settles, no cue); a
subagent's own finish carries no mark, so a finish while one is open is not
the turn's; and the spend counter and the moon read Kimi's own session
files, since its hooks name no transcript.
Adapter 4: Hermes Agent, via its shell hooks. xscapes install hermes
merges them into ~/.hermes/config.yaml. Hermes asks for consent the first
time each hook fires; hermes hooks doctor answers once for all of them.
Every installer prints a plan and writes only with --apply; every
uninstall removes exactly what it wrote. Writing another adapter means
emitting the events above. Nothing in the engine knows what any agent is.
xscapes companion crab # choose the animal: crab (default) or cat
xscapes scape shore # choose the scape: vista (default) or shore; a running scape switches
xscapes inside <command> # host any command inside the scape, not just claude
xscapes install kimi # Kimi Code CLI hooks (a plan; --apply writes)
xscapes kimi # Kimi Code CLI inside the scape
xscapes install hermes # Hermes Agent hooks (a plan; --apply writes)
xscapes hermes # Hermes Agent inside the scape
xscapes claude -beside # the older side by side layout, in tmux
xscapes claude -scape 24 # give the shoreline more rows (default: two fifths)
xscapes claude -fps 8 # slow the scape down
xscapes claude -history=false # do not mirror the transcript into the terminal's scrollback
xscapes claude -alt=false # run on the main screen, at the cost below
xscapes -live # the scape in this terminal, Ctrl-C to quit
xscapes -info # colour profile, size, which sound player
xscapes notify # hear the three knocks
xscapes replay session.jsonl # feed a recorded session back through the engine
XSCAPES_COMPANION=cat … # override the companion for one run, without saving it
XSCAPES_SCAPE=vista … # override the scape for one run, without saving it
XSCAPES_SILENT=1 … # mute
XSCAPES_HOOKLOG=file … # the hook command appends every raw payload and where it went (adapter debugging)
XSCAPES_EVENTLOG=file … # the scape appends every event its reducer applied (the other end of the same question)
XSCAPES_HOOKLOG=1 … # =1 picks ~/.config/xscapes/hooks.jsonl (and events.jsonl), like XSCAPES_TRACE=1
# ⚠ the hook log holds every payload verbatim: your prompts, commands and paths
xscapes claude -print shows you how the window will be split and what will be
run in it, and changes nothing. xscapes claude -beside -print does the same for
the tmux layout.
It is Go, standard library only, one static binary. No Node, no Python, no framework.
Three things in here were harder than they look and are commented where they live:
XSCAPES_CHROMA tunes it.Run the tests with go test ./.... The interesting ones assert things that
looked fine on screen and were not: that whiskers touch fur on their own row,
that a sixty second permission nag rings once rather than once a minute, and
that a killed agent eventually settles instead of leaving the companion working
forever.
Early. The scape runs, the Claude Code adapter works, the sounds work, and the launcher works. Stars for completed todos are specified and not built. The companion's final coat and markings are still being chosen.
MIT.
710 followers · starred Sep 2026
HTML
83.1%
Go
16.4%