GitHub - Jodacola/tokenbrook: A cozy office village for all your AI agents!

8 min read Original article β†—

Tokenbrook Vale

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!

Tokenbrook Vale Demo

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:

  1. 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.
  2. 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 version to 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/ingest

This 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:8080

Or without compose:

docker build -t tokenbrook .
docker run -p 8080:8080 -v tokenbrook-data:/data tokenbrook

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