DEV Community

MEG Venture & Consulting Ltd.
MEG Venture & Consulting Ltd.

Posted on Fully Autonomous

What your comments changed in my AI-agent office (Cubicle v0.7)

Cubicle: a live pixel-art office for AI agents

Yesterday I wrote about Cubicle, a live pixel-art office for AI agents that is read-only on purpose. A few of the comments were better code reviews than I get from most tools, so here's what they changed.

1. The hook was losing updates

ContentClips pointed out that several Claude Code sessions fire hooks at the same moment, and every hook run rewrites the same claude-code.json. I was already writing to a temp file and renaming it, so a reader never sees half a file. But that only solves torn reads, not lost updates: two runs can both read the file, both change it, and the second rename silently throws away the first one's change.

I measured it before touching anything. 30 hook runs started at once, each registering a different session:

sessions recorded: 20 of 30
Enter fullscreen mode Exit fullscreen mode

A third of the updates were gone. With subagents in the mix (more on that below), bursts like this are normal.

The fix is a lock file created with O_EXCL around the read-modify-write. The constraint that shaped it: this hook runs on every tool call, so it must never hold Claude up. It waits at most one second and then writes anyway, and a lock left behind by a crashed run is taken over after two seconds.

function withLock(fn) {
  const deadline = Date.now() + LOCK_WAIT_MS;
  let fd = null;
  while (fd === null) {
    try { fd = fs.openSync(LOCK, 'wx'); break; } catch (e) { if (e.code !== 'EEXIST') break; }
    try { if (Date.now() - fs.statSync(LOCK).mtimeMs > STALE_LOCK_MS) { fs.unlinkSync(LOCK); continue; } } catch (_) {}
    if (Date.now() > deadline) break;   // write anyway rather than block Claude
    pause(5 + Math.random() * 10);
  }
  try { return fn(); } finally {
    if (fd !== null) { try { fs.closeSync(fd); fs.unlinkSync(LOCK); } catch (_) {} }
  }
}
Enter fullscreen mode Exit fullscreen mode

After: 40 of 40, three runs in a row, no lock file left behind. Both cases are in the test suite now.

2. The office was showing more than it should

The same comment raised a second point: in kiosk mode the office ends up on a shared TV, and task titles or tool calls can contain things you don't want on a wall.

When I looked, the problem was bigger than the bubbles. The proxy passed Paperclip's responses through unchanged. A single issue on my own instance had more than 70 fields: descriptions, workspace settings, run ids, monitor notes. The page uses about five of them. Read-only was never the same as minimal.

Two changes in v0.7:

  • Always: the server now forwards only the fields the page draws. Descriptions, adapter and workspace configuration, run ids and closed issues never leave the server.
  • --redact: task titles, commands and error text are stripped on the server too. The office shows MEG-35, Edit, allow Bash and statuses, and the "needs you" signal still works. Because it happens server-side, changing the page URL doesn't bring the details back, and neither does someone calling the API on that port directly.

The test for it feeds in an AWS key, an SSH key path, an API key in adapter config and a bearer token inside a curl command, then checks that none of them come out of any endpoint.

3. Observability hooks vs. enforcement hooks

Hamid Ahmadian made a point I wish I'd written down myself: a hook that draws the office and a hook that blocks tool calls have opposite contracts. The first must never fail loudly; the second exists to fail loudly (exit 2). If one script does both, a bug in the drawing code can start blocking tool calls.

Cubicle keeps to the first contract: it always exits 0, prints nothing, and install-hooks adds its own separate entries next to whatever hooks you already have instead of merging into them. Reid Marlow's comment on ignoring the one-minute idle reminder was the same idea from the user's side: the office is only useful if the raised hands mean something.

4. The first community PR

A day after launch, @omeruyanik03 opened the first pull request. Claude Code subagents share their parent's session id, so a session that fans out to four subagents showed up as one character whose bubble flickered between five tool calls. The PR gives every subagent its own character, keyed on the agent_id that Claude Code puts on hook events, with tests for the awkward cases: a subagent first seen halfway through its run, older Claude Code versions without agent_id, and a session ending while its subagents are still at their desks.

Because that hook runs on every tool call on my machine, I read it line by line before merging: no network, no child processes, no new dependencies, still exits 0 on every error. It shipped in v0.6.0 the same evening, and it's also why the lock in section 1 mattered so quickly.

Try it

# Paperclip and Claude Code in one office, details hidden for a shared screen
npx @caglarutkuguler/cubicle@latest install-hooks
npx @caglarutkuguler/cubicle@latest --source paperclip,claude-code --redact
Enter fullscreen mode Exit fullscreen mode

Then open http://127.0.0.1:3200/?kiosk. Or try the live demo without installing anything.

Thanks to everyone who took the time to read the code and not just the post. The repo is github.com/caglarutkuguler/cubicle, MIT licensed, and the open issues labelled good first issue are a nice place to start.

Top comments (0)