Tokenbrook Vale
A live pixel-art village of your (and others') Claude Code agents at work! By simply installing a small anonymizing CLI and connecting it to your Claude Code via hooks, you can see your agents and subagents come to life as adorable little pixel art characters in the cozy office park of Tokenbrook Vale!
The hooks are purpose-built to support Claude Code, but it's a simple API that could be integrated into other agentic systems just as easily. Have fun!
Demo site
Head on over to https://demo.tokenbrook.com, where I currently have my own agent sessions pushing for demo purposes.
You can even configure your own hook to point at the demo site by using the following ingest URL: https://demo.tokenbrook.com/ingest
See Quickstart - Join as a participant to connect.
Also, don't come at me too hard if my little home server falls over. π
What is this?
This repo is two things that share one set of sprites:
- The live installation β Claude Code users install a tiny hook; their fully-anonymized session/subagent activity streams to a small Go web service and is rendered as a top-down, pixel-art office per session β characters (subagents) walk in, sit to work, and leave, live across everyone participating.
- The Tokenbrook character generator β a Python tool that renders randomly-colored character variations from layered grayscale sprite sheets (animated walking GIFs + sitting PNGs + contact cards), and packs them into the PixiJS texture atlas the frontend uses.
generator (build.py) ββ> web/assets/ (atlas: characters.png + .json + manifest.json)
β consumed by
Claude Code hook β> tokenbrook-hook βPOSTβ> tokenbrook-server βSSEβ> browser (PixiJS)
(anonymizes locally) (in-mem world + SQLite log)
How it works / Privacy
Privacy is the core design constraint:
- Anonymization happens entirely on your machine, in the hook, before any
network call. Identifiers become opaque HMAC-SHA256 tokens keyed by a random
per-install salt that never leaves the machine:
office_id = HMAC(salt, session_id),char_id = HMAC(salt, subagent_id),look = HMAC(salt, agent_type). The server and other viewers only ever see opaque tokens + timestamps β no session ids, agent names, paths, or prompts. - The hook (
tokenbrook-hook) is fire-and-forget: 250 ms timeout, all errors swallowed, always exits 0, so it can never slow down or break Claude Code. - The server (
tokenbrook-server) is one static Go binary: it validates and ingests events, applies them via a single writer goroutine to an in-memory world, appends them to an append-only SQLite log (WAL) for restart recovery, and fans them out to browsers over SSE. It also serves the static frontend. - The frontend (
web/) is PixiJS v8 loaded from a CDN β no build step.
Events
| Claude Code hook | Wire event | Rendered as |
|---|---|---|
SessionStart |
office_open |
a new office appears |
SessionEnd |
office_close |
the office is removed |
SubagentStart |
char_enter |
a character walks in and sits down |
SubagentStop |
char_leave |
the character stands and walks out |
UserPromptSubmit |
lead_busy |
the office's lead works (heads-down) |
Stop |
lead_idle |
the lead turns toward you, dimmed |
Quickstart
A. Join as a participant
You join by installing a tiny hook on your machine that streams anonymized session activity to a running Tokenbrook Vale server.
What you need before you start:
- Claude Code β the thing being visualized.
- Go 1.25+ β the hook binary is built from source on your machine
(
go versionto check; install from https://go.dev/dl/). - Python 3 β used by the install script to edit JSON config files (any recent version; preinstalled on macOS and most Linux distros).
- macOS or Linux β the install script is bash.
- The server's ingest URL β from whoever runs the vale you're joining (or your own; see Self-host).
No Go or Python knowledge is needed β they're only required to be installed.
Install:
git clone https://github.com/Jodacola/tokenbrook.git
cd tokenbrook
deploy/install-hook.sh --url https://demo.tokenbrook.com/ingestThis builds tokenbrook-hook with your local Go toolchain and installs it to
~/.local/bin/tokenbrook-hook, writes ~/.config/tokenbrook/config.json (the
server URL plus a random per-install salt), and registers six hooks in
~/.claude/settings.json: SessionStart, SessionEnd, SubagentStart,
SubagentStop, UserPromptSubmit, and Stop. Start a new Claude Code
session and your office appears in the village.
The hook honors a TOKENBROOK_URL env override; otherwise it reads the config
file. The default URL is http://localhost:8080/ingest.
Uninstall
This unregisters the hooks from ~/.claude/settings.json and removes the
binary. Your config (including the per-install salt) is kept so a reinstall
keeps the same anonymous identity; add --purge to remove that too.
B. Self-host
Docker (easiest):
docker compose up -d # builds the image, serves on http://localhost:8080Or without compose:
docker build -t tokenbrook .
docker run -p 8080:8080 -v tokenbrook-data:/data tokenbrookBare local dev:
cd server go build -o bin/tokenbrook-server ./cmd/tokenbrook-server cd .. STATIC=web ./server/bin/tokenbrook-server
Open http://localhost:8080. With no traffic yet the village is empty β see
Demo without Claude Code below, or install the
hook pointed at http://localhost:8080/ingest.
For a real deployment (VPS, systemd, TLS via Cloudflare Tunnel or Caddy), see
deploy/README.md.
Frontend controls: scroll = zoom (toward the cursor) Β· drag = pan Β·
f = re-frame all.
C. Regenerate sprites (the character generator)
web/assets/ is committed, so cloning works without Python. You only need this
if you change the sprites in assets/.
Requires Python 3.11+ (see .python-version); the only dependency is Pillow.
python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python build.py --atlas-only # regenerates web/assets/The atlas is deterministic by design: with no --seed, the atlas export
always seeds the RNG with 0, so re-running it reproduces the exact same
characters (the committed web/assets/ doesn't churn on rebuilds). To roll a
fresh random cast for maximum enjoyment, pass a different seed each time:
.venv/bin/python build.py --atlas-only --seed $RANDOM(Only the atlas works this way β the GIF/card outputs in output/ are truly
random on every run unless you pin them with --seed.)
build.py renders character variations from the layered grayscale sheets in
assets/, driven by char-builder-data.json. For each hair style it generates
N variations; each variation picks one random color theme (skin / shoes /
pants / shirt / hair) and applies it identically to both the walking
(animated GIF) and sitting (still PNG) poses, so the same character is
consistent across poses. Layers are recolored with a multiply blend
(out = grayscale * color / 255, alpha preserved).
.venv/bin/python build.py [options]
| Flag | Default | Description |
|---|---|---|
--generations |
5 |
Variations per hair style |
--duration |
120 |
Milliseconds per frame (animated GIFs) |
--seed |
(random; atlas: 0) |
RNG seed; the atlas always seeds (0 if unset), so vary it for new characters |
--scale |
1 |
Nearest-neighbor upscale factor |
--gap |
20 |
Px gap between card cells (not scaled) |
--data |
char-builder-data.json |
Path to the config file |
--assets |
assets |
Source sprite-sheet directory |
--out |
output |
Output directory |
--atlas |
(off) | Also emit a PixiJS atlas |
--atlas-only |
(off) | Emit only the atlas, skip GIFs/cards |
--atlas-count |
10 |
Number of characters packed in the atlas |
--atlas-out |
web/assets |
Where the atlas is written |
Making new sprites live (Docker)
The Docker image bakes web/ (including the atlas) in at build time, so a
running container keeps serving the old sprites until you rebuild the image:
.venv/bin/python build.py --atlas-only --seed $RANDOM # regenerate web/assets/ docker compose up -d --build # rebuild image, restart
Docker's layer cache keys on file content, so --build reliably picks up the
changed atlas; if you ever suspect a stale cache, force a from-scratch build
with docker compose build --no-cache && docker compose up -d. Event history
is safe across rebuilds β it lives in the tokenbrook-data volume, not the
image. If the village still looks the same in your browser afterward, it's the
browser cache: hard-refresh the page.
Reference
Server configuration (environment variables)
| Var | Default | Meaning |
|---|---|---|
PORT |
8080 |
listen port |
STATIC |
web |
directory of static assets to serve |
DB_PATH |
tokenbrook.db |
SQLite event log (created if missing) |
TTL_SECONDS |
900 |
reap offices idle longer than this (covers crashes) |
INGEST_BUFFER |
1024 |
ingest channel capacity (overflow = dropped) |
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/ingest |
POST | accepts a JSON event (application/json, 4 KB body cap); returns 202 |
/events |
GET | SSE stream: full snapshot on connect, then live deltas; ping every 25 s |
/ |
GET | the static frontend |
The server sends no CORS headers; the frontend uses a relative
EventSource('/events'), so serve the frontend and API same-origin (or behind
one proxy). /ingest is unauthenticated by design β this is an open art
installation.
Demo without Claude Code
With a server running on localhost:8080:
scripts/demo.sh [count] [delay]
Sends synthetic office traffic β count demo offices (default 1), starting one
every delay seconds (default 5). Watch offices open, characters walk in, sit,
and leave.
Repo layout
build.py character generator (Python, Pillow)
char-builder-data.json generator config (layers, palettes, hair styles)
assets/ layered grayscale sprite sheets (generator input)
output/ generator output (GIFs, cards) β not used by the server
web/ static frontend (PixiJS v8 via CDN, no build step)
web/assets/ committed texture atlas + manifest (generator output)
server/ Go module `tokenbrook`
server/cmd/tokenbrook-server the web service (ingest, SSE, static files)
server/cmd/tokenbrook-hook the Claude Code hook (local anonymization)
deploy/ install scripts, systemd unit, Caddyfile, deploy guide
scripts/demo.sh synthetic demo traffic
Dockerfile, compose.yaml container build / one-command self-host
Running tests
cd server && go test ./...
Covers anonymization determinism, world lifecycle + TTL reaping, and the SQLite store (append/replay/persistence/prune).
Credits
Some of the sprite art in assets/ is based on assets from
LPC Revised (Liberated Pixel Cup Revised)
by ElizaWy, used under the
CC-BY 3.0 / OGA-BY 3.0
licenses. Thank you to ElizaWy and the LPC contributors for making these
freely available.
License
The code is MIT. Sprite art derived from LPC Revised (see Credits) remains under CC-BY 3.0 / OGA-BY 3.0.

