cd /news/ai-agents/what-your-comments-changed-in-my-ai-… · home › topics › ai-agents › article
[ARTICLE · art-141194] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

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

A developer released Cubicle v0.7, a pixel-art office visualization for AI agents, after community code review exposed a lost-update race condition in its Claude Code hook. Measuring 30 concurrent hook runs showed only 20 of 30 sessions recorded; adding an O_EXCL lock file around the read-modify-write raised it to 40 of 40 across three runs. The update also adds a server-side --redact flag that strips sensitive fields from proxied responses and gives each Claude Code subagent its own character keyed on agent_id.

by read4 min views1 publishedSep 28, 2026

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.

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

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
    (5 + Math.random() * 10);
  }
  try { return fn(); } finally {
    if (fd !== null) { try { fs.closeSync(fd); fs.unlinkSync(LOCK); } catch (_) {} }
  }
}

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

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:

--redact: 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.

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.

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.

npx @caglarutkuguler/cubicle@latest install-hooks
npx @caglarutkuguler/cubicle@latest --source paperclip,claude-code --redact

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @cubicle 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/what-your-comments-c…] indexed:0 read:4min 2026-09-28 · —