Runcastle

Blog Post Pipeline

MIT0 downloads

by runcastle

v1.0.1

Turn a single topic into a publish-ready blog post through three sequential Claude Code passes on one warm branch: an Opus strategist outlines the angle and keywords, a Sonnet writer drafts the full post, and an Opus editor tightens the prose and optimises it for search.

Topology

Disclosures

Disclosures — declared side-effect surface

Everything below runs on your machine or inside the sandbox when you use this workflow. Mismatches between these declarations and the actual code block publishing.

Host hooks

Commands executed on YOUR host machine by Sandcastle lifecycle hooks.

None declared.

Sandbox hooks

Commands executed inside the sandbox container.

None declared.

Network access

None. The agents operate only on the local repository inside the sandbox; the post is written from the supplied topic, with no web fetching.

Shell expansion

No shell-expansion blocks in prompt files.

Files

Diff vs the stock Sandcastle 0.12.0 template Dockerfile — green lines were added by the author, red lines were removed from stock.

+# Sandbox image for the Blog Post Pipeline workflow.
+# Node 22, git, and the Claude Code CLI, running as a non-root `agent` user.
+# All three passes (outline, draft, edit) use the same Claude Code runtime.
FROM node:22-bookworm
# System dependencies.
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
jq \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
-# Claude Code CLI (the agent runtime).
+# Claude Code CLI (the agent runtime for every pass).
RUN npm install -g @anthropic-ai/claude-code
# Non-root agent user. `sandcastle docker build-image` aligns AGENT_UID/GID to
# the host user via --build-arg to avoid permission errors on bind mounts.
+# node:22-bookworm already ships a "node" user at UID/GID 1000, so we RENAME it
+# (the stock Sandcastle template pattern) — groupadd/useradd would collide with
+# the existing IDs on a default build.
ARG AGENT_UID=1000
ARG AGENT_GID=1000
-RUN groupadd --gid ${AGENT_GID} agent \
- && useradd --uid ${AGENT_UID} --gid ${AGENT_GID} --create-home --shell /bin/bash agent
+RUN groupmod -o -g ${AGENT_GID} node \
+ && usermod -o -u ${AGENT_UID} -g ${AGENT_GID} -d /home/agent -m -l agent node
-USER agent
-WORKDIR /workspace
+USER ${AGENT_UID}:${AGENT_GID}
+WORKDIR /home/agent
+
+# Sandcastle bind-mounts the worktree and sets the working directory at
+# container start; the container just needs to stay alive until then.
+ENTRYPOINT ["sleep", "infinity"]
Show full Dockerfile (highlighted)
# Sandbox image for the Blog Post Pipeline workflow.
# Node 22, git, and the Claude Code CLI, running as a non-root `agent` user.
# All three passes (outline, draft, edit) use the same Claude Code runtime.
FROM node:22-bookworm

# System dependencies.
RUN apt-get update && apt-get install -y --no-install-recommends \
      git \
      curl \
      jq \
      ca-certificates \
 && rm -rf /var/lib/apt/lists/*

# Claude Code CLI (the agent runtime for every pass).
RUN npm install -g @anthropic-ai/claude-code

# Non-root agent user. `sandcastle docker build-image` aligns AGENT_UID/GID to
# the host user via --build-arg to avoid permission errors on bind mounts.
# node:22-bookworm already ships a "node" user at UID/GID 1000, so we RENAME it
# (the stock Sandcastle template pattern) — groupadd/useradd would collide with
# the existing IDs on a default build.
ARG AGENT_UID=1000
ARG AGENT_GID=1000
RUN groupmod -o -g ${AGENT_GID} node \
 && usermod -o -u ${AGENT_UID} -g ${AGENT_GID} -d /home/agent -m -l agent node

USER ${AGENT_UID}:${AGENT_GID}
WORKDIR /home/agent

# Sandcastle bind-mounts the worktree and sets the working directory at
# container start; the container just needs to stay alive until then.
ENTRYPOINT ["sleep", "infinity"]

README

Blog Post Pipeline

Give it one topic; get back a publish-ready blog post. This workflow runs the same three passes a good content team does — plan, write, edit — but as three specialised agents working on one warm branch, each building on the last one's committed work.

What it does

Most AI blog drafts are mediocre because a single prompt tries to plan, write, and polish all at once. This pipeline splits the job:

  1. Outline (Opus) — a strategist picks a sharp angle, chooses a primary and secondary keywords, drafts title options, and lays out a section-by-section skeleton with the concrete example each section must use. Committed to content/outline.md.
  2. Draft (Sonnet) — a writer reads that outline and produces the full 1,100–1,600 word post in clear, active prose, following the structure exactly. Committed to content/post.md.
  3. Edit + SEO (Opus) — an editor cuts 10–20% of the words, fixes weak verbs and passive voice, verifies the thesis is argued, then optimises on-page SEO: a ≤60-char title with the keyword, a meta description, keyword placement, and a clean heading hierarchy. Edited in place at content/post.md.

How it works

main.ts opens one warm Docker sandbox with createSandbox() on the content/blog branch, then calls sandbox.run() three times in sequence. Each pass is a distinct agent (Opus → Sonnet → Opus) but shares the same container and branch, so pass two reads pass one's outline off disk and pass three edits pass two's draft. The topology is a straight line: outline → draft → edit.

The topic is a single {{TOPIC}} argument substituted into every prompt file, so you change what gets written by editing one constant — no prompt surgery.

Requirements

Set CLAUDE_CODE_OAUTH_TOKEN in .sandcastle/.env (run claude setup-token on your host). Edit the TOPIC constant at the top of .sandcastle/main.ts to your subject. Build the image once with npx @ai-hero/sandcastle docker build-image, then run the pipeline with npx tsx .sandcastle/main.ts. The finished post is at content/post.md, with the planning outline preserved at content/outline.md.