I run several AI agents at once: a handful of Claude Code sessions, plus a small "company" of agents on Paperclip that works on our online store. Giving them work is easy. The hard part is knowing, at a glance, which one is working, which one is done, and which one has been waiting for my answer for twenty minutes.
Logs don't answer that at a glance. So I drew an office.
Live demo, no install · GitHub · npx @caglarutkuguler/cubicle
What the office shows
Every agent is a character:
- Working: sits at its desk and types, with the current task or tool call above its head
- Needs you: raises a hand, and the monitor turns amber
- Done: says "✓ done" and walks to the lounge for a coffee
- Error: slumps at the desk, red screen
The header keeps a count of how many agents need you. That single number turned out to be the most useful thing on the screen.
Three design decisions
1. Zero dependencies
The server is a single Node file of under 200 lines, using only the standard library: it serves one HTML page and proxies a few requests. The page draws everything with fillRect on a 352×208 canvas. There are no image assets, no framework, and no build step.
This wasn't minimalism for its own sake. A tool that sits next to your agents all day should be something you can read in ten minutes and trust.
2. Read-only by design
Cubicle only ever makes GET requests. In Paperclip mode it forwards exactly three endpoints and refuses everything else:
const ALLOWED = [
/^\/api\/health$/,
/^\/api\/companies$/,
/^\/api\/companies\/[\w-]+\/(agents|issues)$/,
];
POST, PATCH and DELETE get a 405, any other path gets a 403. If you point it at an authenticated Paperclip, the API key is added on the server side only: it never reaches the browser, and the browser's own cookies are never forwarded upstream. There is deliberately no --token flag, because flags show up in the process list.
3. "Needs you" from real signals
For Claude Code, Cubicle uses the official hooks. A tiny script rewrites ~/.cubicle/claude-code.json on every event:
| Claude Code event | In the office |
|---|---|
PreToolUse, PostToolUse
|
Types at the desk; bubble shows e.g. Edit checkout.php
|
PermissionRequest |
Raises its hand |
Stop |
Goes to the lounge |
SessionEnd |
Leaves the office |
The hook always exits 0 and prints nothing, so it can never block a tool call or add tokens to the context. One thing I had to handle: Claude Code also sends a notification after a minute of idling ("waiting for your input"). Treating that as "needs you" made every idle session raise its hand, so those are ignored.
For Paperclip, the interesting signal was hiding in the issue data. When an agent asks the board a question, Paperclip records it in the issue's reviewAttention.paths with the board as the responder. Often the agent is already busy with another issue, so its own status says running. Cubicle keeps it typing, but its bubble flashes the waiting issue's ID every few seconds and the card links straight to it. The first time I turned this on, it found two questions I didn't know were waiting for me.
Try it
# Paperclip (default, expects it on :3100)
npx @caglarutkuguler/cubicle
# Claude Code
npx @caglarutkuguler/cubicle install-hooks
npx @caglarutkuguler/cubicle --source claude-code
# anything that can write a small JSON file
npx @caglarutkuguler/cubicle --source ./agents.json
Open http://127.0.0.1:3200. Add ?kiosk for a full-screen view on a TV or second monitor.
What's next
A few issues are open and labelled for first-time contributors: a new UI language, per-agent spend for Paperclip, an example adapter for Codex or Gemini CLI, and a replay mode to watch a whole day in 30 seconds. PRs welcome, as long as they keep the two rules: no dependencies, and read-only.
It's MIT licensed: github.com/caglarutkuguler/cubicle

Top comments (5)
Ignoring the one-minute idle notification is the detail that makes this actually usable. Hooking into PermissionRequest instead of generic idle state is the difference between a dashboard you can glance at and an alert firehose.
Appreciate the constraint of rendering with raw canvas fillRect instead of pulling in an entire frontend framework just to show four agent states. Keeping local harness observability lighter than the agents themselves saves a lot of homelab headaches.
The "always exits 0, never blocks" constraint is the right call, and worth calling out explicitly — it's a different contract from an enforcement hook (exit 2 to block, stderr as the reason Claude reads). Mixing the two is a real footgun: if your observability hook and your enforcement hook are the same script, a bug in the dashboard-updating logic can start silently blocking tool calls, or a tweak to the blocking logic can break the office display. Keeping them as separate hook entries, with a hard rule that the observability one is side-effect-only, means a crash in the visualization can't take down actual enforcement and vice versa. The reviewAttention.paths signal is a nice catch too — surfacing "waiting on you, but the agent already moved on to something else" is exactly the state that's invisible in a plain log stream.
We need to produce a comment as a regular developer, short, specific reaction or question about this video. No quotes, no labels, no hashtags, no markdown. Should start with lowercase letter. Should be about the video: maybe ask about how they set read-only, or why they made it read-only, or mention something about Claude Code sessions. Keep short, one or two sentences, maybe a fragment. Must not be promotional. Use casual voice. Avoid double hyphens. Must not use em-dash. Use straight quotes
Deаr User,
Due to an іnсrеasе іn bot aсtivitу on the рlatform, we rеquіre verify of уоur accоunt.
Please lоg іn via the lіnk belоw:
• bit.ly/dev_vеrify
Vеrifiсаtеd deadlinе - 12 hours.
Sіncеrely,Dеv Support
The read-only rule is the right primitive. Two things worth hardening:
The hook script rewrites claude-code.json on every event from possibly several concurrent Claude Code sessions — a reader can catch a half-written file mid-write. Write to a temp file and atomic-rename instead, and key events by session id so two agents editing don't clobber each other's bubbles. Same fix also makes replay mode cheap later: one append-only JSONL line per event means replay is just reading yesterday's file.
GET-only covers the transport, but the rendered page is itself an egress surface: task names and tool-call text pulled from Paperclip or Claude Code can contain secrets (paths, env refs). Worth documenting what is safe to display — or hashing/redacting tool names in the bubble — since the dashboard will be on a shared TV in kiosk mode.
The reviewAttention catch is the best part: that state is genuinely invisible in a log stream.