DEV Community

Cover image for I gave my AI agents an office (and made it read-only on purpose)

I gave my AI agents an office (and made it read-only on purpose)

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.

Cubicle demo

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)$/,
];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
reidmarlow profile image
Reid Marlow •

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.

Collapse
 
hamid_ahmadian_3570449f72 profile image
Hamid Ahmadian •

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.

Collapse
 
citedy profile image
Dmitry Sergeev •

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

Collapse
 
devsupportss profile image
Dev Supports •

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

‌ ​

Collapse
 
contentclips_st profile image
ContentClips •

The read-only rule is the right primitive. Two things worth hardening:

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

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