Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

poke-agents

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 display modes on a mocked desktop

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.

Sprite states at three sizes

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.


Contents


Requirements

  • 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

Install

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.app

Every 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 --watch

The 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 doctor

How it works

agent 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. SessionEnd doesn't fire on a crash or kill -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 print would quietly pollute your conversation. Diagnostics go to ~/.claude/poke-agents/hook.log.

Display modes

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.

Sizes

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.

Sprite states

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.

Background agents

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.

What does not get a sprite

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, and shiny rates

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.

Shiny

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.

Clicking a sprite

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 runningdonerunning 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.

Labels

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.

Adding your terminal

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 session

There'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.

Other agent harnesses

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 reference

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

Configuration

~/.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)

Troubleshooting

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.

Uninstall

./cli/poke-agents uninstall     # removes only its own hook entries
rm -rf ~/.claude/poke-agents    # state, sprite cache, config, logs

Then quit the app from the menu bar and delete PokeAgents.app.

What this installs, and what it can do

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 fetch downloads from play.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.

Sprites and licensing

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.

Contributing

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

About

Your running coding agents, as animated Pokemon on your macOS desktop. Click a sprite to jump to its terminal. Works with Claude Code, OpenCode, goose and Crush.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages