Some of the scheduled posts on this site were written while nobody was awake. A scheduler wakes an agent at a fixed hour; it picks topics, writes, runs the build, commits, and opens a pull request. What a human sees in the morning is one finished PR.

The hardest question when wiring this up wasn't about the prompt. It was: when this agent gets something wrong, how much can it break?

1. What Makes Unattended Different

An agent you're chatting with is remarkably safe. When it heads somewhere strange, you stop it in the very next message. Not because the agent is good, but because the gap between mistake and detection is a few seconds.

On a schedule, that gap is hours — and nothing pauses in between. One wrong judgment, and every subsequent decision is built on top of it.

interactive: decide → human sees → decide → human sees → ... unattended: decide → decide → decide → decide → ... → human sees

So the thing to improve in unattended work is not the accuracy of decisions but the structure between them. You can't eliminate wrong calls; you can decide how far one propagates.

2. Four Layers That Contain the Blast Radius

Four boundaries decide how far a wrong call travels. Each stops something different, and none of them covers for a missing one.

Layer Boundary Stops Worst case if absent ────────────────────────────────────────────────────────────────────────────── 1 Credentials reachable repos spread to other repos a repo out of scope changes 2 Write paths editable files global config/layout damage the whole site breaks 3 Merge the base branch unreviewed changes landing bad posts on develop 4 Deploy the live site unreviewed publication removing indexed URLs

The cost of undoing rises by an order of magnitude per layer. Caught at layer 1, nothing happened. At 2, revert a commit. At 3, you're editing the base branch's history. Past 4, you're dealing with URLs a search engine has already indexed.

So the test for a guardrail wasn't "does this prevent the mistake?" but "if it doesn't, which layer catches it?" Mistakes happen regardless. What matters is where they stop.

The layers don't substitute for each other.

"We gate on PRs, so the write paths can stay wide" is the common mistake. A pull request guarantees a human looks; it does not guarantee a human notices. A one-line change to eleventy.config.mjs buried among eight posts is easy to miss.

3. Fix the Write Boundary First

The first thing settled wasn't "what can it do" but "where can it write."

may write: src/ko/posts/*.njk src/en/posts/*.njk .content-queue.md may not (untouched unless explicitly asked): eleventy.config.mjs the build guards themselves vercel.json redirects _includes/, _data/ layouts and global meta

There's a principle behind the split. A bad post file makes one post wrong. A bad layout or config makes the entire site wrong at once.

Excluding eleventy.config.mjs matters most. That file holds the build guards that enforce this repo's rules. If the thing being checked can edit the checker, the guard isn't a guard — the cheapest fix for a failing build becomes "delete the check."

What you open up to an agent is a question about reversibility, not capability.

More access does mean more can get done. It also means more can quietly break. Doing good work and being easy to undo are separate axes.

4. Output Must Not Equal Deployment

The second guardrail is the pull request. The agent pushes only to its own branch, never writes to the base branch, and never merges its own PR.

the agent does: create branch content/auto-YYYY-MM-DD commit, push open a PR against develop the agent does not: merge the PR push to develop trigger a deploy

That single line is the difference between "posts publish themselves" and "drafts prepare themselves." In the second, the worst case is closing one useless PR. In the first, the worst case is a bad post live on the site.

The cost difference is negligible — skimming a PR over coffee takes minutes. The cost of undoing differs by orders of magnitude. A deployed URL can already be indexed, and from there even removal is work.

5. Let a Machine Decide What "Done" Means

With no human present, something has to judge completion. Let the agent judge for itself and it will almost always report success.

So completion is defined as a command, not a sentence. What goes in the document and what becomes a check follows the same test as writing the rules file — if a machine can judge it, it doesn't belong in prose.

Run npm run build after finishing each post pair. If it fails, fix it and run again. Nothing gets committed until it passes.

This works because the build genuinely checks content: that both ko and en files exist, that their publish dates match, that no post links forward to a post published later. Those are precisely the things an agent gets wrong most often.

Worth noticing: none of those checks were built for an agent. They exist because humans kept making those same mistakes by hand — and they carry over unchanged as the agent's definition of done. Guards that are good for people are good for agents.

Rewriting conditions until a machine can judge them

Write out completion conditions and most first drafts turn out to be unjudgeable sentences. Converting them was the actual work here.

Unjudgeable Rewritten as Judged by ────────────────────────────────────────────────────────────────────────── "is the post good?" build passes + length floor build · wc "do ko and en correspond?" slug, date, category all match build guard "are the links valid?" target exists + publishes ≤ mine build guard "is the format right?" required front matter present build (ref error) "is the work finished?" every queue item marked [x] reading a file "is it reversible?" changes stay inside allowed paths git diff --name-only

The first row never fully converted. "Is it good?" isn't machine-judgeable, so we fixed a floor a machine can check and handed the rest to PR review. Push to automate the whole judgment and you end up keeping only what's easy to judge — that isn't setting a standard, it's changing one.

The last row was added later. Pulling the changed-file list with git diff --name-only and halting on anything outside the allowed paths turns the write boundary from an instruction into a check. Instructions get forgotten; checks don't.

6. Half-Finished Should Still Leave Half Behind

Unattended runs get cut off — time limits, network, sometimes nothing you can name. Write all eight posts and commit once at the end, and seven of them evaporate.

So the commit unit is one post. A pair that passes the build is committed and pushed right there. Never batched.

The matching entry in .content-queue.md is marked done in that same commit, because progress and output have to share a commit or they drift apart. That design is covered more fully in resumable agent tasks.

What you gain isn't only resumability. The bigger win is that whatever survives a dead session is always a valid state. A branch cut off after three posts isn't "a failed run" — it's a three-post PR.

7. Commit the Plan Before the Work

This rule came later and paid off more than expected: the round's plan is committed and pushed before a single post is written.

1. survey existing posts (topics, dates) 2. write this round's plan into .content-queue.md 3. commit and push the plan ← before any post 4. then start writing

The reason is to leave something behind even when the run dies at zero posts. With a plan in the repo, the next run picks it up. Without one, the next run re-derives everything from scratch — and risks choosing the same topics again.

The side effect turned out to be more useful than the intent. When a human reviews the PR, the reasoning for those eight choices is right there in the repository. You're recording not just the decisions but their basis, so when the content drifts off course you can find where it started.

8. How to Record a Failure Nobody Saw

When a run fails at 3am, the fact that it failed disappears with it. Logs die with the session, and what's left in the morning is the absence of a PR. But an absent PR looks identical whether the run failed or simply had nothing to do.

We compared four ways of leaving a trace.

Method Human sees Next session reads Cost ──────────────────────────────────────────────────────────────────── Exit silently no no 0 — and nothing remains Run logs if they dig no 0 — dies with the session Empty PR / issue yes no a notification fires Note in queue file via the PR yes one commit

We settled on writing it into the queue file and pushing that commit. It's the file the next session is guaranteed to read, and it still surfaces to a human through the PR or the commit history. Opening an issue is louder, but the deciding factor was that the next session never reads issues.

What goes in the note is fixed too: what it tried, how far it got, and what a human would have to do to unblock it. Without the last part, the morning starts by re-reading logs.

"Nothing to do" and "failed" must not look the same from outside.

Both produce no PR. Without a way to tell them apart, a job that runs daily can be quietly dead for weeks before anyone notices.

9. What We Chose Not to Automate

The exclusions matter as much as the guardrails.

  • Merging. Humans do it. Add auto-merge and every mechanism above loses its meaning.
  • Editing already-published posts. Pushing a live post's date into the future removes it from the site — a deletion that happens silently, so a person decides.
  • Touching the build guards. The checked doesn't edit the checker.
  • Auto-starting a new round. Finish the previous round first; if there's nothing to resume, exit having done nothing. Not manufacturing work is a feature.

That last one especially. Unless you spell out "if there's nothing to do, stop," an unattended agent will find something to do. Work invented that way is usually work nobody needed.

How we chose what stays manual

The list matters less than the test behind it, which runs on two axes.

Cheap to undo Expensive to undo ────────────────────────────────────────────────────────────────────── Visible immediately automate it automate, but gate it (drafting a post) (open a PR; a human merges) Surfaces late automate, but log it don't automate (queue file updates) (publish dates · guard edits)

The bottom-right cell is what humans do. Changing a published post's date sits exactly there — under scheduled publishing, moving a date into the future removes that post from the site on the next build, with no error and no warning. Expensive to undo (the indexing is already gone) and slow to surface.

Humans touch this in exactly three places

Human involvement condensed to three points. Nowhere else.

  • Reviewing the plan — the plan commit sits at the front of the PR in the morning. A wrong direction gets caught here.
  • Merging the PR — the one mandatory intervention. Automate this and every other mechanism becomes decoration.
  • Handling blocked items — anything the queue file marks blocked needs a human decision to move.

All three are asynchronous, which is the important part. The agent never sits waiting on a person. Whenever a human looks, the state is in the repository, and the next run picks up from there. Design "human confirmation" as a synchronous step in an unattended run and it stops being unattended.

10. Common Questions

Why not automate the merge too?
For work a check can judge, do. The problem here is that the output is prose. The build only sees form — not whether a fact is wrong, an argument is muddled, or a point is made twice. When the quality that defines the output is the part a machine can't judge, merging stays human. For something like a dependency bump, where tests stand in for judgment, auto-merge is perfectly reasonable.

How do you scope the agent's credentials?
To exactly what the job uses: write access to this one repository. No other repos, no deploy hooks, no third-party keys. Narrowing access isn't a statement of distrust — it's deciding the radius of an accident in advance, for the same reason least privilege applies to human accounts.

Doesn't a daily run pile up pull requests?
It would. That's what the round concept prevents: if a round is unfinished, no new round opens until it's done, and if a PR already exists the run adds commits to it instead of opening another. Without that, every day produces a new branch and a new PR, and they accumulate faster than anyone reviews them.

How would you know if the agent broke a rule?
It surfaces in two places: the build catches what the build catches, and the rest shows up in the PR diff. Making the second one real takes a habit — read the changed-file list before the diff itself, asking only whether anything sits outside the allowed paths. That check takes a human seconds, which is why it landed before any automation of it.

How did you pick the run time?
It starts while people sleep so the result is waiting in the morning. The hour matters less than the slack between the run and the review. If a human has to look the moment the run ends, every delayed run turns into a person waiting.

Isn't all this excessive?
Counted up, it's one branch, one PR, one build command, and one queue file. Nothing new was built — existing pieces were just put in order. The excessive option is the other one: running with no boundaries at all and reviewing every result from scratch each morning costs far more.

11. Summary

  • The safety mechanism in interactive use is a human. Unattended, structure has to fill that seat.
  • Narrow the write boundary first — down to where a mistake spoils one post, not the site.
  • Keep output from being deployment. The pull request is the gate.
  • Define done as a command, not a sentence. The build passes, or it isn't done.
  • Keep commits small so a run cut in half leaves a valid half.
  • Say explicitly that doing nothing is an acceptable outcome.
  • Record failures too. "Nothing happened" and "it failed" must not look the same from outside.
  • Keep human checkpoints asynchronous. The moment the agent waits on a person, it isn't unattended.

Running an unattended agent changed the question we ask. It isn't how much do we trust it — it's how cheaply can we undo it. When undo is cheap, limited trust is enough to ship. When undo is expensive, no amount of trust ever feels like enough.