Your running coding agents, as Pokémon on your desktop.
Every session gets a sprite. It wanders your screen while the agent works, stops and waves an exclamation mark when the agent needs you, and curls up asleep when the turn is done. Click one to jump straight to the terminal running it.
Works with Claude Code, OpenCode, goose and Crush out of the box, and adding another harness is a small map of event names.
Four of the twelve arrangements, drawn at the defaults a fresh install uses: Cluster — Bottom Right at the small sprite size. They look modest against a full screen because that is the point — both are configurable from the menu bar.
States, left to right: running, attention, done. Sizes, top to bottom:
large, medium, small.
Both images are generated, not captured. The wallpaper and window shapes are drawn by the app in an offscreen render, the session names are invented, and no real screen appears anywhere — but the sprite positions come from the actual layout engine, so the arrangements are exactly what you get. Regenerate them with
POKEAGENTS_RENDER_MODES=out.png PokeAgents.app/Contents/MacOS/PokeAgents.
The point is peripheral vision. If you run several agents at once, the thing you actually need to know is which one is waiting on you — and you need to know it without alt-tabbing through terminals to check. A sprite that stops moving and starts flashing in the corner of your eye answers that question for free.
- Requirements
- Install
- How it works
- Display modes
- Sizes
- Sprite states
- Species, and shiny rates
- Clicking a sprite
- Labels
- Adding your terminal
- Other agent harnesses
- Command reference
- Configuration
- Troubleshooting
- Uninstall
- What this installs, and what it can do
- Sprites and licensing
- macOS 13 or later
- Xcode Command Line Tools (
xcode-select --install) — full Xcode is not needed - Python 3.8+ (macOS ships this)
- At least one agent harness: Claude Code, OpenCode, goose or Crush
git clone https://github.com/urnotsam/poke-agents.git
cd poke-agents
./overlay/build.sh # builds PokeAgents.app (~30s, no Xcode required)
./cli/poke-agents fetch --all # caches the roster (~118 MB, one time)
./cli/poke-agents install # Claude Code, plus terminal adapters
# --harness opencode | goose | crush | all
open overlay/PokeAgents.appEvery installer backs up any file it edits, marks the entries it adds, and
removes exactly those on uninstall — see
what this installs. For Claude Code
that file is ~/.claude/settings.json; the others write their own config, and
OpenCode gets a plugin file.
Sessions already running won't have sprites until they next do something. To see them immediately:
./cli/poke-agents adopt --watchThe two sources cooperate. adopt steps aside for any session that already has
a hook-written record, and the overlay draws only one sprite per process even if
both records exist — a session adopted before its hook first fired is described
twice on disk, and poke-agents prune clears the leftover. Hooks label a session
by its repository; adopt can use the terminal's title, which is often more
specific.
Check everything landed:
./cli/poke-agents doctoragent session Claude Code, OpenCode, goose, Crush
│ lifecycle events (start, tool, needs-user, idle, end)
▼
pokeagents_hook.py Python, standard library only
│ one atomic JSON write
▼
~/.claude/poke-agents/sessions/*.json one file per live session
│ FSEvents
▼
PokeAgents.app Swift menu-bar agent
│ one borderless window per sprite
▼
your desktop
The filesystem is the message bus. No server, no database, no socket, no network listener. Each half works with the other absent: the hook writes files whether or not the overlay is running, and the overlay happily animates hand-written JSON.
Two consequences worth knowing:
- Dead sessions get cleaned up automatically.
SessionEnddoesn't fire on a crash orkill -9, so the overlay also checks every 10 seconds whether each recorded pid still exists and despawns the ones that don't. Without that, the display slowly fills with ghosts and stops meaning anything. - The hook is built to stay out of your way. It always exits 0 and never
writes to stdout — some harnesses feed hook stdout back into the model's
context, so a stray
printwould quietly pollute your conversation. Diagnostics go to~/.claude/poke-agents/hook.log.
Twelve arrangements, from the menu bar icon under Display. The default is Cluster — Bottom Right, which keeps sprites out of your working area; the marquee modes are livelier but travel across whatever you are looking at.
| Group | Modes | Behaviour |
|---|---|---|
| Marquee | Top, Bottom, Left, Right | Sprites travel along the edge and wrap around |
| Static | Top, Bottom, Left, Right | Evenly spaced along the edge, holding position |
| Cluster | Top Left, Top Right, Bottom Left, Bottom Right | A three-column grid tucked in the corner |
Four of the twelve are pictured at the top of this page.
All twelve come from one geometry model rather than twelve special cases. The rule that makes it work: sprites wander only across their line of travel, never along it. Wandering along the line would let neighbours close the gap and overlap; wandering across it never can. In the marquee modes every sprite also travels at exactly the same speed, so the even spacing assigned once is spacing kept forever.
Twelve modes × three sizes × five screen sizes are covered by ~302,000 assertions checking that sprites never overlap, never leave the screen, never jump when their state changes, and — in the cluster modes — that the labels don't collide either. That last one exists because generating the screenshots above is what revealed the labels overlapping while the sprites technically didn't.
Small (44pt), Medium (58pt), Large (72pt), from the menu bar under Size. Small is the default — the overlay is meant for peripheral vision, and a corner cluster of large sprites takes up real screen.
Gen 5 sprites are around 96px natively, so even Large is a slight downscale and everything stays crisp — sampling is nearest-neighbour, because smoothing pixel art is the fastest way to make it look cheap.
| State | What it means | What you see |
|---|---|---|
running |
The agent is working | Idle animation loops, sprite drifts along |
attention |
The agent is waiting on you | Fast bob, orange ! bubble, pulsing glow, label at full brightness |
done |
The turn finished | Static sprite, dimmed to 60%, zZz drifting up |
attention is the state the whole thing exists to surface, so it also wins any
contest for screen space: if more sprites are live than fit, an attention
sprite evicts a done one rather than being hidden itself. Each harness reports
it slightly differently — Claude Code's Notification, OpenCode's
permission.asked — but it always means the same thing: the agent is blocked on
you.
A background agent (claude --bg, or a forked session) has no controlling
terminal, so there is nothing to click through to. These are hidden by
default — they cannot be acted on, so they would only compete for attention
with the sessions that can.
They are not forgotten. The menu bar shows N background (no terminal) while any
are live, and clicking that — or Show background agents in the menu — draws
them. When shown, they carry a circle-with-slash badge and an outlined label,
clicking them shakes, and the right-click menu says why rather than offering a
Focus item that would do nothing.
"showHeadless": true in config.json makes that permanent.
The badge is drawn rather than a colour treatment: opacity already means done
and the glow means attention, so those channels are taken, and a drawn mark
does not depend on colour vision.
Subagents — the Task fan-outs an agent spawns internally — run inside their
parent session and never start one of their own, so they produce no record and
no sprite. The parent simply shows running while they work.
Species is derived from the session id:
species = SPECIES[fnv1a(session_id) % 1134]
Session ids are random, so this feels like a wild encounter — but because it's derived rather than stored, the same session keeps its species across an overlay restart with nothing persisted. If two live sessions would draw the same species, the second probes forward to the next free one, so you never see doubles.
The roster is 1,134 sprites: every species Pokémon Showdown has Gen 5 artwork for in all four variants — animated, animated shiny, static, static shiny — which is what the three states need between them. Regional forms, megas and Gigantamax are in, so Alolan Raichu and Galarian Ponyta are their own encounters.
Female variants are the one deliberate exclusion. They differ from the base by a handful of pixels, so at 72pt they read as the same creature and would only make two sessions harder to tell apart.
Caching all of it is about 118 MB across 4,536 files. You don't have to:
anything not cached falls back to a drawn Poké Ball, and poke-agents fetch <species> takes named species if you would rather keep it small.
A sprite has a 1 in 64 chance of being shiny (about 1.6%), decided by a second hash of the session id. Like the species, it's derived rather than rolled, so a shiny session stays shiny for its whole life and across restarts.
That is far more generous than the games: 1 in 8192 in Gen 2–5, 1 in 4096 from Gen 6 on. Those odds are tuned for a game you play for hundreds of hours. At 1 in 4096 you would start roughly ten sessions a day and see your first shiny in about a year, which is not a feature, it's a rumour. At 1 in 64 you'll see one every week or two — rare enough to be a small event when it happens, common enough to actually exist.
To change it, edit SHINY_ODDS in hook/pokeagents/species.py.
Set it to 1 if you want everything shiny, which is a legitimate aesthetic
choice and looks quite good in the cluster modes.
Left click focuses the terminal running that session — the sprite is a
launcher, not just a status light. See the ! in the corner of your eye, click
it, you're in the session that needs you.
If no adapter can focus the session, the sprite shakes instead. That mostly
happens when no adapter recognises your terminal — run poke-agents terminals
to check. Background agents genuinely have nowhere to jump to, which is why
they are hidden by default.
Right click opens a menu for that specific session:
| Item | What it does |
|---|---|
| (header) | The session's label, state, last tool used, and directory |
| Focus Session | Same as a left click |
| Copy Working Directory | Puts the session's cwd on the clipboard |
| Reveal in Finder | Opens that directory |
| Hide This Sprite | Mutes it until that session needs you |
| Display / Size | The same submenus as the menu bar |
Hiding is a mute, not a dismissal — but only attention brings a sprite back.
That distinction matters: a working session cycles running → done → running
on every tool call, so un-muting on any change at all would make hiding useless.
Muting still cannot lose an alert, which is the property worth keeping. The menu
bar shows a Show N hidden item while any are muted.
The label carries a session's identity, since the species is random. Two sources:
- From hooks — the git repository name, plus the branch for a worktree
(
acme@hotfix). - From
adopt— the terminal's title, which is usually what you asked the agent to do.
Titles are phrased as instructions, so they get compressed rather than simply cut off. "Implement Granola document demo feature requests" truncated at 20 characters gives you "Implement Granola do…" — all of which every other title also starts with. Dropping the leading verb and the filler words instead gives "Granola document", which is the part that tells them apart.
Labels are capped at 20 characters, wide enough for a repo@branch worktree
label. Change MAX_LEN in
hook/pokeagents/labels.py if you want more or
less.
PokeAgents ships adapters for herdr, Terminal.app, iTerm2, tmux, WezTerm, and Ghostty. Adding your own means writing one small executable — you never touch Swift or rebuild the app.
An adapter is any executable in ~/.claude/poke-agents/terminals/, in any language,
that answers two subcommands via its exit code:
myterm detect # exit 0 if this terminal is usable right now
myterm focus <pid> <tty> # exit 0 if you actually focused the sessionThere's an optional third, discover, which lets poke-agents adopt list sessions
your terminal already knows about before they've fired any hook.
Full contract, worked examples, and testing instructions: terminals/README.md.
Adapters for new terminals are very welcome as PRs.
Only the producer half of poke-agents knows which agent you run. The overlay, the layout, the terminal adapters and click-to-focus all read a generic session record and never learn what created it.
| Harness | Install | Coverage |
|---|---|---|
| Claude Code | poke-agents install |
Full lifecycle |
| OpenCode | --harness opencode |
Full lifecycle, via a plugin |
| goose | --harness goose |
Full lifecycle |
| Crush | --harness crush |
Partial — see below |
| Anything herdr or tmux can see | nothing to install | Appears via adopt |
--harness all installs every one it finds.
Crush is partial on purpose. It currently implements only PreToolUse, so a
sprite appears on the first tool call and stays running for the rest of the
session — there is no idle or end event to move it along. It still despawns when
Crush exits, because the overlay reaps sessions whose process is gone. This
improves for free as Crush implements more events. A harness that cannot cover
the lifecycle has to declare that gap, and a test enforces it, so an accidental
omission can't pass for a deliberate one.
Each harness maps its own event names onto six canonical ones — start,
activity, tool, needs-user, idle, end — and that map is the whole
integration. OpenCode's is arguably better than Claude Code's: permission.asked
says specifically that the agent is blocked on you, where Claude Code's
Notification also covers other cases.
You can also skip harnesses entirely. A session record is a JSON file; anything that writes one gets a sprite.
Full contract: harnesses/README.md.
| Command | What it does |
|---|---|
poke-agents install [--harness X] |
Wire up a harness (backs up any config it edits) |
poke-agents uninstall [--harness X] |
Remove exactly what it added |
poke-agents doctor |
Report hooks, state, sprite cache, overlay, and display mode |
poke-agents ls |
List live sessions as a table |
poke-agents adopt [--watch] |
Show sessions your terminal already knows about |
poke-agents fetch --all |
Download and cache the sprite roster |
poke-agents prune |
Delete session records whose process is gone |
poke-agents terminals |
List adapters and which ones detect |
poke-agents simulate [--count N] |
Write fake sessions, for working on the overlay itself |
~/.claude/poke-agents/config.json:
{
"mode": "marqueeTop",
"size": "large",
"terminals": ["herdr", "iterm2", "terminal-app"]
}mode and size are normally set from the menu bar. terminals is optional and
sets adapter priority; unlisted adapters are tried afterwards in alphabetical
order.
Environment variables:
| Variable | Effect |
|---|---|
POKEAGENTS_DISABLE=1 |
Hook does nothing — turn it off without uninstalling |
POKEAGENTS_HOME |
Move the state directory (default ~/.claude/poke-agents) |
No sprites appear. Run poke-agents doctor. Hooks only fire on a session's
next event, so try typing something, or run poke-agents adopt --watch.
Clicking does nothing. Run poke-agents terminals to see whether any adapter
detects. If yours isn't listed, write an adapter — it's
about ten lines. Check ~/.claude/poke-agents/overlay.log, which records every
click, the pid and tty it resolved, and whether focusing succeeded.
Clicking switches the tab but doesn't bring the window forward. Your adapter's
focus is doing half the job. It needs to select the tab and activate the host
application; this is the most common mistake when writing one.
Sprites are Poké Balls. The sprite cache is empty or the download failed. Run
poke-agents fetch --all.
A sprite is stuck. The overlay reaps sessions whose process is gone within 10
seconds. If one persists, its pid is genuinely still alive — check with
poke-agents ls, which flags stale records.
Sprites cover something important. Switch to a cluster mode, or Small, or hit Pause in the menu bar.
./cli/poke-agents uninstall # removes only its own hook entries
rm -rf ~/.claude/poke-agents # state, sprite cache, config, logsThen quit the app from the menu bar and delete PokeAgents.app.
Worth being explicit, because this asks you to run code on every Claude Code event.
poke-agents install adds six entries to ~/.claude/settings.json, one per hook
event, each running hook/pokeagents_hook.py from wherever you cloned this repo.
Claude Code hooks are arbitrary shell commands, so anything in that script runs
with your user's full privileges, on every event, in every session. That is
true of any Claude Code hook, and it's worth understanding before installing one
from the internet — including this one.
What this hook actually does: reads the event JSON on stdin, resolves the owning
process, and writes one small JSON file. It makes no network requests. The only
component that touches the network is poke-agents fetch, which downloads sprites
over HTTPS from play.pokemonshowdown.com when you run it explicitly.
Everything stays on your machine. Nothing is sent anywhere. Session labels can
include your working-directory and terminal-title text, so they're written only
to ~/.claude/poke-agents/ and drawn on your own screen.
The installer backs up your settings file first, marks its own entries so
uninstall removes exactly those and nothing else, and refuses to touch a
settings file it can't parse. One side effect worth knowing: because it rewrites
the file atomically through a temp file, settings.json ends up owner-only
(0600) even if it was more permissive before.
Three more things a security review of this project surfaced, which you should know rather than have to discover:
- The session directory is user-writable and unauthenticated. Anything
running as you can drop a file into
~/.claude/poke-agents/sessions/and make a sprite appear. Values read back from those files are therefore treated as untrusted: adapters receive the pid and tty as arguments and never as shell or AppleScript text, and a species that isn't a plain Showdown id is refused rather than used to build a file path. If you write an adapter, keep that property — never interpolate a session field into a command string. - Sprites come from a third-party CDN.
poke-agents fetchdownloads fromplay.pokemonshowdown.com. Responses are pinned to that host across redirects, size-capped, and checked for GIF/PNG magic bytes before being cached, but they are not signature-verified. - The overlay asks for Automation permission so it can control your terminal via AppleScript. That grant is what makes click-to-focus work; denying it leaves everything else functional.
There are no third-party dependencies — the Python side is standard library only, and the Swift package has no external packages — so there is no dependency supply chain to audit beyond this repository itself.
The code is MIT licensed. See LICENSE.
The sprites are not mine, and are not in this repository. PokeAgents ships a
downloader, not artwork. poke-agents fetch pulls sprites at runtime from
Pokémon Showdown into a local cache
on your own machine. The only bundled image is a Poké Ball, drawn in code as a
fallback.
Pokémon and Pokémon character names are trademarks of Nintendo, Creatures Inc., GAME FREAK Inc., and The Pokémon Company. This is an unofficial fan project, unaffiliated with and unendorsed by any of them, intended for personal use. The Gen 5 animated sprites for later-generation Pokémon were made by the Smogon community; if you plan to do anything with these assets beyond running this on your own desktop, talk to Smogon first.
Bug reports, terminal adapters, and species-roster arguments all welcome. See CONTRIBUTING.md.
Run the tests:
cd hook && PYTHONPATH=. python3 -m unittest discover -s tests
cd overlay && swift run PokeAgentsTests
