cd /news/ai-agents/i-accidentally-built-a-dark-software… · home topics ai-agents article
[ARTICLE · art-133031] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

I Accidentally Built a Dark Software Factory. Here's How.

A developer describes accidentally building a "dark software factory" called Holodeck after struggling to keep Claude Code running for extended periods without stopping or suffering context drift. The solution involved feeding the LLM one task at a time from a checklist, an approach the developer later recognized as a version of the "Ralph loop.

by read12 min views1 publishedSep 17, 2026

This is the first in a series of articles about my dark software factory, Holodeck. This one covers how it came to be and the principles underneath it. In future articles I'd like to dive into particular aspects in more detail. Want to know more about something I've written here? Hit me up on LinkedIn or Twitter and tell me what to write about next.

Three weeks ago, I read Steve Yegge's post on Fences, not Sandboxes, on a rules- or laws-based covenant rather than strict guardrails, and promised to write up my own dark factory journey. Yesterday, I read Seats and Sunsets, on the fuel crisis that hits when you're burning tokens faster than you can afford them. Now I really have to get to it. Not that this is a competition, but because I want to share my thoughts and experiences. Maybe it'll help someone. Maybe you'll have a better idea and share that with me.

My factory came about a little by accident. I suppose a lot of them do, but by the time I realised I was writing one, it was already halfway done. And needed to be rewritten. Let's rewind to the beginning.

Around May of 2025, I was trying to work out how to get Claude Code, my preferred LLM harness, to run for more than a few minutes. I would create a mini-spec for a feature, get Claude to break it down into tasks, and ask it to do one at a time. Claude would diligently make a recommendation of how to do this, a bit like the "plans" we see it create nowadays, and get going.

But there were two issues. The first is that Claude would stop after a few minutes, because it wanted to make sure I wanted to continue. Even adding "Keep going forever. Make no mistakes." didn't work 🤣 The second issue, if it didn't stop right away, was that it would start suffering from context drift and rot. It would compact and lose the detail of the todo list. Or it would forget that it was supposed to complete the todo list at all. Long running agents seemed impossible.

I realised that I would need to do one of two things. I would either need to stop Claude from stopping, or stop it from forgetting. And either seemed impossible!

It might have been the gym workout. Or the shower. Or the nap. Or taking the dog for a walk. Whatever it was I was doing that wasn't working on my Claude problem, and that allowed my brain to get out of its own way, I had that "aha" moment we all get when all we want to do is stop the gym workout or shower or whatever and try something out.

If I couldn't get Claude to remember everything in one go, could I create a mechanism to feed it one item at a time? A fresh context window for just one bit of work? But Claude needed to know all the work, and if I cleared the context window it wouldn't know where it got to. And if I documented it, it would have to keep a pointer to the next task. That was it! A checklist that Claude would pull an item off, complete, and mark off. Or fail, not mark off, and try again. I didn't know it at the time, but I had just discovered my own version of what we affectionately call the Ralph loop today. It was rough and still failed, but it was better, and better was going in the right direction. Over the next few days I started working with Claude to improve it:

Writing this now, it seems so obvious, but that's the thing with learning, and time. We forget the struggles we overcame and only see the ones ahead of us. But that's probably the topic of a future article.

So how did I get from that to a software factory?

Aside: you might know I'm an avid fan of anything IoT. I built a full-sized collapsible stage door that fit in a suitcase so I could bring it with me around the world to demonstrate using OAuth to unlock an Arduino powered NFC-based lock. I was nothing if not committed to the bit. You would therefore not be surprised to hear that my home has a fair amount of automation, and the most used one, given Heather's and my travel, is the garden irrigation system. We currently have 14 watering zones, around 60 separate soil containers requiring sprinklers and soil moisture sensors, and irrigation pipe routing that would give Grand Central Station a run for its train tracks. The thing I hate though? Home Assistant's user interface. It's a pain. Even the plugins and expansions don't make it much better. And certainly not pretty!

Having considered installing Claude Code on my Home Assistant's Raspberry Pi, it suddenly occurred to me! (Yes, another aha moment. I was probably in line to pay for groceries.) What can Claude do with Home Assistant's URL and an API key 🤔 But not as a skill. I certainly wasn't about to self-leak my key into context. But what if...

I created a new NextJS app, opened Claude Code, and typed something along these lines:

This is a fresh NextJS installation.

Create a .env.example containing CLAUDE_CODE_OAUTH_TOKEN, HOMEASSISTANT_URL, and HOMEASSISTANT_API_KEY. Then remove all existing files from the default NextJS app, and replace the homepage entirely with a basic instant messaging interface.

The messages show up at the top between two actors: 'user' and 'assistant'. The input field under the message list allows the user to type a message and pressing enter or clicking the send button will send it to a local API endpoint.

To start with, the message list is blank. When the user sends a message to the API endpoint, a system call is made to claude -p to execute the request, and the message is added to the message list and shown to the user as a 'user' message. The response from claude -p is added to the message list and shown to the user as an 'assistant' message.

Then create a docker compose configuration file to have this application run in a docker container as a non-root user, with the non-root user having full read and write access to the application source code.

Something like that, anyway. And off it went. And once it was done, I had a very simple wrapper around Claude Code running in a web application.

I created .env. I docker upped. I loaded the website.

The input box dared me to enter something.

Create a dashboard that shows me a list of batteries that need replacing

It was suitably vague. The app didn't know anything. No system prompt. Just an environment to scan and interpret. After some whirring, discombobulating and cogitating, it discovered the environment variables and made a curl request to Home Assistant to see what it could find! Moments later I had a rewritten web site with navigation, a dashboard, and a list of devices running low on juice. It also brought the API key into context 🙈

ARIA today: the dashboard, the garden valves, and an About dialog with immaculate vibes

This is the moment the factory was born. I was now talking to a self-modifying web application, and trying to get it to harden itself.

Today it cannot read credentials (it uses nfi-tools, a CLI tool and MCP server for handling sensitive information without exposing values to AI assistants, instead), errs on the side of creating deterministic routines rather than relying on inference and harness tools to induce action, has multi-channel chat, and each chat's self-modification happens in git worktrees with deterministic guardrails that stop merging unless all tests and preflight checks pass. It also decided to change NextJS to run compiled instead of in run dev mode for speed and efficiency, and called itself ARIA.

By March 2026, I had built a handful of harnesses. Some were wrapped in a web app. Some were global npm modules that ran as sidecars to a project. One even ran entirely in GitHub Actions and issues. Some worked better than others. Some just didn't work, or became hard to maintain. But they all converged back on the original premise: a spec, an implementation plan, a build loop, and guardrails.

Spec, plan, loop, guard. Every harness I built converged on the same four powers

So it was in early July when I thought to myself, how would I make a dark software factory actually work? So far I've had to tend to my factories. The lights have been very much on. But the types of remedies I've been implementing to keep momentum going have all had a similar outline. An agent wasn't sure how to proceed. It became cautious. (Some say that Anthropic's models are trained from a mindset of caution, whereas OpenAI's from a mindset of adventure, so perhaps adding some OpenAI models might have helped?)

I decided to think about how orchestration was working. I had one main agent that would talk to the "client". That was me. I was also the co-founder of the factory, but that was a separate role. While playing the client, I would answer non-technical questions to help define a web application's requirements. Until now, I'd been working "manually" with Opus, and later Fable, to convert these into specs and plans. The build loop was autonomous, but the speccing and planning steps needed to happen autonomously now too.

Another aside: my first job out of uni was for a marketing agency in London. After moving to Australia and working in engineering roles for product companies, I ended up at another marketing agency in Melbourne. I didn't feel like my work had purpose, but I did enjoy the fact that I'd only work on something for a few weeks or months and then get a new challenge. I guess that's why I love consulting and DevRel so much. Always a new challenge. But anyway, marketing agencies. They both had very similar processes for getting from client requirements to shipped product, and it went like this:

It's a well-tested process, and the beauty is that each person in the workflow has a well-defined role and a set of action items they can take. It's a human finite state machine. And that's what my factory needed.

This is how Steve seems to be using his "seats" analogy. Persistent offices that an agent embodies, defining the role's responsibilities, capabilities, and history, so a fresh session doesn't have to derive them from scratch. Just like the org chart I'd worked inside 20 years ago.

This is probably the widest deviation from Steve's position I have. It's no chasm, more of an Omaha sinkhole. Steve's factory, Wheelhouse, grew to over 400 rulings, 185 rule rows in CLAUDE.md, and 650 refusal sites across 173 scripts before it stopped working. He cut it to 14 fences and now personally approves any new one.

I went down the route of purely deterministic guardrails. Agents don't read the rules, they're applied outside their context and nothing merges unless all of them pass. And models are selected not for the task type, but for the certainty and blast radius of that task. Tasks are given to Sonnet with a time budget of 20 minutes, except in two conditions: (1) the task has a high blast radius, in which case it's given to Fable to implement, or (2) the task is novel or ambiguous work, in which case it's given to Fable with a time budget of 10 minutes to write the tests and interfaces first and stop at a checkpoint, or a question, rather than commit to an implementation.

Weaker models behind an external gate can't ship a bad merge, and can only cost me retries. So the rules align with the risk of the ticket, not the model. And model selection is not based on trust. It's based on the cost per merged ticket.

I still call it a proof-of-concept. I'm building some demos with the factory, and of course the factory builds itself now too, all from specs taken from a non-technical human interviewee. But the rest of it? That's all done by agents.

I still have to poke them with a stick sometimes, but less so every day, because the Kanban board does so much. It changes the agents' behaviour. When an agent got stuck before, it had no out. Now, it knows it can move a task to "blocked" with a note, and a more capable agent will pick it up. Or perhaps the agent identifies a problem outside the scope of their ticket, something they depend on; they can create a new ticket which gets routed to the right agent instead of quietly widening their own task. QA can raise a ticket for a regression they found while testing something else. The backlog grows the way a real team's does.

A demo project's board, with an incident ticket the factory raised itself after the gate rejected the same diff three times

Holodeck started out as an experiment to see where agentic development would hit the limit of autonomy, but the more time I spend on it, the less I find myself necessary.

In the next article in this series I'll open up Holodeck's ticket board and walk through the state machine in detail: which agent is allowed to make which move, what happens when a ticket bounces between "ready" and "blocked" a few too many times, and the deterministic guardrails that sit between "in review" and "done". That's where most of the poking with a stick used to happen, and it's where most of the interesting lessons live.

Building your own factory, or wondering whether you should? Find me on LinkedIn or Twitter. I'd love to hear what's working for you, what isn't, and what you'd like me to dig into next.

Originally published at bendechr.ai.

── more in #ai-agents 4 stories · sorted by recency
── more on @claude code 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/i-accidentally-built…] indexed:0 read:12min 2026-09-17 ·