I built two robots that refuse to write my blog for me
I built two opencode agents to run my blog, one to mine my work for ideas and one to write them up, then spent most of the week teaching them, patiently, everything they were forbidden to touch.
I write code across a handful of projects, and every so often I think: this would make a decent blog post. Then the thought dies quietly, somewhere between "later" and "I've forgotten what I even did." So I did the only reasonable thing a developer does when faced with a discipline problem. Instead of fixing my habits, I built a tool. Two of them, actually: two agents in opencode, one that digs through my recent work and proposes what to write about, and one that actually writes it.
This is the story of that build. Spoiler: most of the effort went not into teaching the agents what to do, but into teaching them what they must never do.
Why two agents instead of one
The temptation was to build a single all-knowing agent: "read my week, write my blog." I resisted, and I'm glad I did, because the two jobs are genuinely different animals.
The first job is analysis: figure out what is worth writing about. It reads git history and my past coding sessions, correlates them, and produces a shortlist. It should never touch prose. The second job is creative writing in my voice, turning one of those ideas into an article, and it should never go rummaging through databases. So I split them:
- topic-extractor: a read-only analyst. It surfaces candidate topics from recent activity and writes them to a file. It never writes an article, even if I ask it to.
- blog-writer: the writer. It takes one topic and drafts a bilingual article, iterating with me. It never goes near the source projects except to read them.
Two agents, two narrow jobs, two very different sets of things they're forbidden from doing. That separation turned out to be the whole point.
Teaching an agent to observe without touching
The topic-extractor's raw material is my own history: git commits, plus the transcripts of my past opencode sessions, which live in a local SQLite database. Which means its actual job is to go dumpster-diving through everything I did last week and come back holding something up hopefully, like a raccoon that has found half a sandwich. Some weeks the sandwich is a genuinely good idea. Some weeks it's three commits titled "fix", "fix again" and "ok now really fix", and a session where I argued with a type error for forty minutes and lost.
The commits are easy: git log in a date window. The sessions were more interesting. They sit in ~/.local/share/opencode/opencode.db, and I very much did not want an "analyst" that could accidentally rewrite my own history like a time traveller with commitment issues. So every single database call is made with sqlite3 -readonly. Not "usually." Not "unless the file looks locked." Always. The agent is instructed to surface a clear error rather than ever fall back to write access.
Then it correlates: commits and sessions that happened close together in time, in the same project, probably belong to the same story. From that it writes a DRAFT list of candidate topics, each with a working title, a note on why it's interesting, its sources, and (a later addition) a little scorecard.
There was one unglamorous but essential step in the middle: redaction. Coding sessions are exactly the kind of place a stray API key or bearer token likes to hang out, quietly, waiting for its moment. Before anything gets written to disk, the session text runs through a small filter that swaps secret-looking strings for a redaction marker and raises a flag:
$ printf 'Authorization: Bearer <a-very-secret-token>' | python3 redact.py
Authorization: Bearer [REDACTED:bearer]
# stderr: FLAG
Any topic built from flagged material gets a visible warning, so I can decide in review whether it's safe. It's conservative and a little paranoid, which for anything touching secrets is precisely the correct amount of paranoid.
The DRAFT-first ritual
Both agents follow the same rule, and it's the rule I'd keep if I could keep only one: write the file first, then ask.
The agent produces its output, a topics list or an article, as a file on disk, explicitly marked as a draft. Only then does it turn to me with a short question: does this look right? It never dumps the whole thing into the chat for approval.
There's a boring technical reason and a good design reason. The boring reason: stuffing a few kilobytes of markdown into an interactive prompt can terminate the request outright, which is a spectacular way to lose work. The good reason: the file on disk is the single source of truth. I review the artefact, not a summary of the artefact, which is the difference between reading a contract and having a stranger describe it to you over the phone from a moving train. The draft marker (Status: DRAFT for topic lists, draft: true in the frontmatter for articles) flips to final only when I explicitly say so. The agent never promotes its own homework.
The part where I mostly wrote rules about saying "no"
Here's the confession. I set out to build two helpful writing assistants. I spent most of my time building two extremely well-behaved ones. The guardrails ended up being the real product.
The strongest guardrail is a permission boundary that opencode enforces at the runtime level, not just in a prompt the agent can be sweet-talked into ignoring. Each agent can read widely, because it has to, but its ability to write is fenced into specific folders:
"edit": {
"*": "deny",
"**": "deny",
"inputs/**": "allow"
}
That's the topic-extractor: deny everything, then allow exactly one folder. The blog-writer gets three (drafts/, inputs/, published/) and not a byte more. The catch-all denials come first and the narrow allow comes last, because the last matching rule wins. Get that ordering backwards and your careful fence becomes a decorative gate standing alone in a field, fooling nobody.
The same treatment applies to the shell:
"bash": {
"*": "ask",
"git *": "allow",
"sqlite3 -readonly *": "allow",
"mv drafts/* published/*": "allow",
"git push*": "deny",
"git commit --amend*": "deny",
"rm -rf*": "deny"
}
git and read-only sqlite3 are allowed, anything else asks first. The blog-writer may run exactly one mv to publish a finished article, and the genuinely alarming commands (git push, git commit --amend, rm -rf) are flatly denied. An agent that can rm -rf on your behalf is not an assistant. It's a liability with good intentions and root access.
On top of the runtime fence, each agent carries anti-hijack rules in its own prompt, a belt to go with the braces. If I get lazy and tell the topic-extractor "oh just write the article yourself," it declines and points me at the blog-writer. If I tell the blog-writer to fix a bug in one of my source projects, or to commit, or to push, it refuses and offers to get back to writing. They are, by design, magnificently unhelpful in all the ways I want them to be unhelpful.
Building it in five steps, each one tested
I didn't write all this in one heroic sitting. It went in five deliberate stages: the project scaffold and permissions first, then the topic-extractor, then my writing conventions, then the blog-writer, and finally an end-to-end acceptance pass tying it together.
Each stage came with a small suite of mechanical checks: does the permission config actually deny rm -rf, does the redaction filter actually flag a fake token, does the writer refuse when my conventions file is still an empty template. Unglamorous tests, the software equivalent of smoke detectors. Silent and ignorable right up until the one evening they earn their entire existence.
What I'd tell myself before starting
The thing that surprised me is how much of "building an agent" is really just deciding the shape of the box it lives in. Extracting topics and writing prose were the easy afternoon. The rest of the week went into scoped writes, read-only database access, the DRAFT-first ritual, the redaction pass, and the stubborn little "no" rules that keep each agent in its lane.
I started out wanting to automate my blog and ended up mostly automating my own restraint. I'm fine with that. And there's a decent test of whether it worked sitting right in front of you: this article was the first thing the machine and I made together. You can judge the tone. I'm just relieved nothing got rm -rf'd along the way.
The whole thing, agents and skills and the paranoid permission config, lives at github.com/Light2288/blog-writer if you want to poke at it or judge my commit messages.