A four-agent AI blog co-author running on my Raspberry Pi

· ai-agents, crewai, self-hosting, raspberry-pi, automation, homelab

I write this blog as Markdown files in a git repo, and a Cloudflare Tunnel + Watchtower pipeline ships them the moment a change lands on main. The missing piece was the writing itself. So I built a co-author: a small crew of AI agents that researches a topic, drafts it in my voice, takes my edit notes, and opens a pull request — running entirely on the second Raspberry Pi in my home.

The important word is co-author, not author. It does the typing; I keep the final say. This post is about the shape of that system and the decisions that keep it safe to leave running on a box in my house.

The core idea

The crew can write and propose, but it can never publish. A pull request is the only door to the live site, and only I open it.

Everything below follows from that one rule. The agents are free to be wrong, creative, or confused — because nothing they produce reaches luckyganesh.in until I read the diff and click merge.

Four agents, one job each

It’s a CrewAI service with four narrowly-scoped agents. Each does one thing, which makes each one easy to reason about and easy to swap.

AgentDoesToolsTriggered by
ResearcherGathers current facts for the topicweb search + page reader (DuckDuckGo, free)Research & write
WriterDrafts the post in my voice— (reads my style guide + recent posts)Research & write
EditorApplies my review notes, keeps the rest intactApply suggestions (loop)
PublisherOpens a pull request to maina deterministic git + GitHub toolPublish

The Researcher and Writer run back-to-back when I start a post: the Researcher produces a brief — facts with their sources, plus an honest list of what it couldn’t verify — and the Writer turns that brief, plus my notes, into a draft. The Editor is a loop: I read the draft, type suggestions in plain English, and it revises precisely what I asked for without silently rewriting the parts that already worked. The Publisher only runs when I’m happy.

High-level flow

   me (phone or laptop, over Tailscale)


   ┌──────────────────────────────────────────────────────┐
   │  lucky-node02  —  blog-coauthor (Flask + CrewAI)       │
   │                                                        │
   │   topic ─▶ Researcher ─▶ Writer ─▶ draft (SQLite)      │
   │                                  │                     │
   │              my notes ─▶ Editor ─┘  (revise loop)      │
   │                                  │                     │
   │                              Publisher                 │
   └──────────────────────────────────│─────────────────────┘
                                       │  git push + GitHub API

                          Pull request ──▶ I review & merge


                    CI builds image ─▶ Watchtower ─▶ live

The model calls go out to OpenRouter; the Pi only orchestrates the crew and holds the state. That split is deliberate — a Raspberry Pi has no business running an LLM, but it’s perfectly happy shuttling prompts and parsing JSON. I point the Writer at a strong model and the cheaper Researcher and Publisher at a small one, because research and PR-wording don’t need the good model and I’d rather not pay for it on every call.

Where the safety actually lives

A model writing to a git repo is the kind of thing that sounds reckless until you look at where the boundaries are drawn. Three of them matter:

The net effect: the worst a confused agent can do is open a pull request I’ll reject. That’s a failure mode I’m completely comfortable leaving unattended.

Managing posts in flight

The dashboard tracks each post in a local SQLite database, and every post gets its own branch and its own PR — so I can have several going at once. The interesting part is reconciliation: the dashboard checks the open PRs against GitHub on every load and keeps itself honest.

What I did on GitHubWhat the dashboard does
Merged a post’s PRDrops it from the working set, deletes the branch
Closed a PR unmergedReturns the post to writing so I can revise and re-publish
Opened an edit of a live postRe-publishing updates the same PR, preserves the original pubDate, stamps updatedDate

So the source of truth is the repo, not the app’s memory. If I merge on my phone in the GitHub app, the dashboard catches up the next time I open it. Nothing is left dangling.

Keeping the voice mine

The Writer reads two things before it drafts: a STYLE.md that describes my voice in detail — first person, lead with the idea, em-dashes, honest about trade-offs, no marketing-speak — and my two most recently edited posts, pulled live from the repo. The second part matters more than the first: the style guide is what I say I sound like; the recent posts are what I actually sound like right now. As my writing drifts, the examples drift with it, for free.

How it’s deployed

Same pattern as the website it feeds. GitHub Actions builds an arm64 image and pushes it to Docker Hub; the Pi only ever pulls. On lucky-node02 it’s one docker-compose.yml and a .env full of secrets — the OpenRouter key, a GitHub PAT scoped to only the my-website repo, and an optional password to gate the UI even on the LAN. The cloned repo and the draft database live in named volumes, so restarts don’t lose state.

Crucially, it is not on the Cloudflare tunnel. It’s an admin tool, so it’s private by default — reachable only over my Tailscale mesh at http://lucky-node02:5000. That means I can start a post from my phone on the train, and the internet has no idea the service exists. The whole thing lives on the helper Pi precisely because the public site host should stay boring and untouched.

Why I like this shape

The reflex with AI writing tools is to let them publish and hope. I went the other way: give the agents real autonomy inside a box whose only exit is a pull request I control. It turns an unpredictable model into something I’m happy to leave running on hardware in my house, because the blast radius of a bad generation is a diff I won’t merge.

It also composes cleanly with everything already running — same image-based deploy, same VPN-only admin boundary, same git push → CI → Watchtower → live pipeline. The co-author just adds one more producer at the front of a chain I already trust.

What’s next

A few things I’d still like to add: image suggestions during drafting, a way to queue topics in advance, and a cheap eval that flags when a draft has drifted off my voice before I even read it. For now, though, the loop is complete — I give it a topic and some notes, read what comes back, nudge it, and merge. Including, honestly, posts a lot like this one.

← All posts