# Stop Writing Blog Posts

**How we used Claude to turn our meeting notes, voice memos, and Slack threads into a blog that publishes daily.**

By Vignesh Ramakrishnan, South Arc Digital
April 2026, revised September 2026

> **Second edition note.** The first edition of this book documented the engine running on Claude Code Routines, then in research preview. In July 2026 we moved all scheduling to GitHub Actions and removed the routines (the full story is gotcha #2 in "What bit us"). This edition documents the GitHub Actions architecture the engine actually runs on today.

---

## Table of contents

1. The problem we were solving
2. The architecture in plain language
3. The stack
4. Phase 1: foundation files (data, skills, validator)
5. Phase 2: the writing skill (the most important part)
6. Phase 3: the seven agents
7. Phase 4: SEO essentials that actually move the needle
8. Phase 5: the safety hook (so the engine cannot break things)
9. Phase 6: GitHub Actions setup
10. Phase 7: integrating with a secondary site (or skip if you only have one)
11. What bit us (read this even if you skip everything else)
12. What it costs to run
13. Voice rules: the AI tells we found and how to avoid them
14. What we would do differently
15. Closing: about South Arc Digital

*How to use this book with your agent*
*A note on how this was written*
*Reference bundle*

The book is written to be read cover-to-cover or dropped into an agent's context. If you want the agent to build the system from this playbook, skip ahead to *How to use this book with your agent* after the table of contents; everything else reads fine as a human-only document.

---

## How to use this book with your agent

The fastest way to build what the book describes is to let a coding agent scaffold it. This book is written to be consumed by a human reader and by an LLM in the same pass. Two ways to use it:

### Drop the markdown into your agent's project context

Download the raw markdown from `southarcdigital.com/ebook/stop-writing-blog-posts.md` and drop it into your repo at `context/content-engine-playbook.md` or anywhere your agent already reads. If you use Claude Code, add a line to `CLAUDE.md` or `.claude/settings.json`:

```
Read context/content-engine-playbook.md before any content-engine task.
```

The agent will pull the book into working context whenever you ask it to scaffold, extend, or debug the content engine. The specificity of this guide (exact prompt wording, exact skill file content, exact validator rules) is what lets an agent rebuild the system from the markdown alone.

### Paste the bootstrap prompt to kick off a fresh build

Open a new Claude Code session in an existing Next.js + MDX repo. Paste the following:

```
I want to build the content engine described in the "Stop Writing Blog Posts" playbook
(southarcdigital.com/ebook/stop-writing-blog-posts). Load the playbook into context
first, then scaffold the data files, skills, validator, hook, and scripts into my repo
against the structure described in the Reference Bundle section.

## My setup

- Repo root: (this one)
- Domain: {DOMAIN}
- Blog path: src/content/blog/*.mdx
- Tracks: {list, e.g. journal, guides, nicherly}
- Author: {AUTHOR_NAME}

## Build

1. Data files under content-engine/: planner.csv, affiliates.json, published.json
2. Skills under .claude/skills/: writing-voice.md, seo-audit.md, a track template per track, content-rewriter.md
3. scripts/validate-mdx.js using gray-matter; tests first at scripts/__tests__/validate-mdx.test.ts
4. scripts/notify-indexnow.js plus the public/{key}.txt file
5. .githooks/pre-push with the allowlist regex for my content paths, plus scripts/setup-hooks.sh
6. .github/workflows/content-engine-daily.yml that runs Scout (twice weekly) and Captain (daily) via
   anthropics/claude-code-action on a cron, reading the prompts from content-engine/prompts/, plus a
   reusable writer.yml the captain job hands each picked topic to
7. Next.js SEO wiring: canonical URLs via generateMetadata, ArticleSchema server component, dynamic sitemap

## What NOT to build

- The repository secrets (I'll add CLAUDE_CODE_OAUTH_TOKEN and the Telegram secrets in GitHub repo settings manually)
- Any blog posts yet
- Anything that bypasses the pre-push allowlist

Work on a branch. Commit incrementally. Run npm run build at the end and fix anything that breaks.
```

Fill in the four `{}` placeholders, paste, and let it run. Expect about four hours from scaffold start to first publishable post. The agent will stop and ask when it hits ambiguity; answer with specifics (see the book's voice rules section for what "specifics" means).

### The llms.txt

We publish an `llms.txt` at `southarcdigital.com/llms.txt` that indexes this book and the companion primer in a format LLM crawlers parse natively. If your agent supports URL-based research (Claude Code WebSearch, Cursor @web, etc.), point it at that URL and ask it to read the playbook before starting.

---

## 1. The problem we were solving

Our content engine's first generated blog post opened like this:

> Your phone rings while you're elbow-deep in a burst pipe under someone's kitchen sink.

Further down it said "honestly, if you're a one-person operation," called a pricing comparison "what we discovered," and manufactured a metaphor about one product being "your back office" and another being "your front door." That was the system's first output. We wired the whole pipeline up, pressed go, and produced slop.

This guide is about what we did to stop producing that.

We run South Arc Digital. We build custom AI automation for e-commerce operations, and we run Nicherly, a SaaS product for AI-automation agency owners. Both businesses need content, and neither of us has time to write three posts a week alongside client delivery. We tried the obvious things first: interns (well-researched drafts that read like content marketing templates), Claude and ChatGPT directly (same problem, different surface). Then we built the agent system this guide documents.

The real failure was never at the writing layer. It was at the system layer. Producing content reliably means seven things in sequence:

1. Picking a topic worth writing about
2. Researching it without parroting the top 10 SERP results
3. Writing it in a consistent voice
4. Adding visual elements where they help
5. Inserting affiliate links where the post supports them
6. Validating SEO basics before shipping
7. Publishing and notifying search engines

Most "AI content" setups do step 3 and call it done. We needed all seven, every day, without anyone touching it. And the output had to not read like the burst-pipe paragraph above.

This guide is the build documentation, with every thing that bit us and every voice rule we wrote. It is for agency owners and solo builders who already ship client work, are comfortable with git and markdown files, and want a system instead of another tool. It is not a "10x your traffic" pitch. We have published 10 posts so far. Indexing takes weeks. Authority compounds over months. We will be honest about what has moved and what has not.

## 2. The architecture in plain language

[VISUAL 1: Architecture diagram. Scout + Captain + five sub-agents + data files + skills + safety hook]

The system has two scheduled agents and five sub-agents that get spawned on demand. GitHub Actions does the scheduling: a cron-triggered workflow starts each agent as a Claude Code session on a runner.

**Scout** runs twice a week (Mondays and Thursdays in our setup). It looks at our content planner, sees what topics are queued and what is already published, then researches keyword gaps. It picks 10-15 new topic ideas across our three publishing tracks and adds them to the planner.

**Captain** runs every day. It reads the planner, picks the top 1-2 queued topics by priority, and orchestrates the production. For each topic, Captain spawns a chain of sub-agents:

1. **Writer** (or **Rewriter** if the source is an intern draft) - produces the MDX file with frontmatter, body, and citations
2. **Creative** - reviews the draft and adds visual components (statistics callouts, comparison boxes, tables) where they help
3. **Affiliate Linker** - inserts affiliate links if the post is in a track that supports them, with proper disclosure
4. **SEO Auditor** - validates frontmatter, structure, keyword placement, internal links, em-dashes, word count
5. **Publisher** - if SEO audit passes, commits the file and pushes to main, then notifies IndexNow

The data layer is three files in the repo: `planner.csv` for the content calendar, `affiliates.json` for the partner map, and `published.json` for the registry of what has shipped. These files are the only state the system needs. There is no database.

The voice and quality layer is five markdown skill files: a writing voice skill, an SEO audit skill, a track template per publishing track, and a rewriter skill for processing intern drafts. Each agent reads the relevant skills at the start of its work.

[VISUAL 2: Track → skill → output flow diagram]

The safety layer is a git pre-push hook that physically rejects pushes to main that touch anything outside the content directories. The agents cannot accidentally push code changes, even if the prompt drifts.

## 3. The stack

You need:

- **Claude Code** with a Pro, Max, Team, or Enterprise plan. The agents run inside GitHub Actions through the official `anthropics/claude-code-action`, authenticated with an OAuth token you generate once from that subscription (`claude setup-token`) and store as a repository secret. GitHub Actions is the scheduler; your subscription is the brain.
- **One or two GitHub repos**: one for your main brand site, optionally one for a secondary site that also receives content. If you only have one site, you only need one repo.
- **A Next.js site** with MDX support. The system writes MDX files into `src/content/blog/` and your site renders them. We use `next-mdx-remote` with `remark-gfm` for GitHub-flavored markdown including tables.
- **Vercel** for deployment. The publish step assumes Vercel auto-deploys on push to main. Other hosts work the same way (Netlify, Railway, Cloudflare Pages) but the example commands assume Vercel.
- **Optional: a SERP-scraping MCP** like Brightdata for richer SERP research. The built-in WebSearch tool works as a fallback. We pay for one such service but the system functions without it.
- **Optional: Miro MCP** if you want the Creative agent to generate diagrams.

You do not need:

- A database
- A CMS
- An automation platform like Zapier, Make, or n8n (though they would work if you wanted them)
- Multiple Anthropic API keys (the OAuth token draws from your existing subscription)
- A separate hosting account for the agents (they run on GitHub's Actions runners)

If you already have a Next.js site on Vercel and a Claude subscription, you have everything except the workflow files and one OAuth token.

## 4. Phase 1: foundation files

Create three data files in your main repo at `content-engine/`:

### planner.csv

This is your content calendar. Every row is a topic. The Captain picks rows from here in priority order. The columns:

```
title,keyword,track,content_type,priority,affiliate_potential,serp_competition,status,assigned_agent,started_at,published_at,published_url,notes
```

Track values: `journal` for technical/authority content, `guides` for SEO-focused commodity content, `nicherly` for the secondary brand site. Use whatever track names match your setup.

Content_type is `new` (write from scratch) or `rewrite` (transform an existing draft).

Priority is `high`, `medium`, or `low`.

Status starts as `queued` and progresses through `in_progress`, `published`, `seo_failed`, `failed`, `review_pending` depending on outcome.

Seed it with 10-15 rows. The Captain will start producing immediately on first run.

### How we seeded ours

The planner is the only place in the system that decides *what* to write about. The quality of whatever goes into it determines the quality of every post that comes out. Generic keyword tools produce generic rows, which produce generic posts.

We seeded ours from our own material. We anonymized about fifty artifacts (six months of client meeting transcripts, voice memos we had recorded while thinking out loud about particular builds, internal Slack threads where we had worked through a problem, and a handful of Loom walkthroughs we had made for each other), then fed the clean corpus into Cowork to extract themes and topics. Cowork distilled the fifty inputs into twenty-four distinct topic ideas organized under seven recurring theme categories, which gave us the track assignments for each row. We dropped the twenty-four topics straight into `planner.csv` as the first seeded rows, tagged each one against its theme category, set priorities, and let Scout expand from there on its weekly runs.

The effect on output was large. Posts seeded this way were specific from the first paragraph because the underlying idea came from work we had actually done. Posts seeded from pure keyword research read like every other piece of SEO content.

If you do not use Cowork, the shape of the input matters more than the tool. Any extraction pipeline that takes your meeting transcripts, voice dumps, or written thinking and returns a list of themes will do. Whisper plus a Claude summarization prompt will do. What you want to avoid is starting the planner from a keyword tool's list of "trending queries in your niche," because that is where the genericness enters the pipeline.

### affiliates.json

If you have affiliate partners, map their keywords here. Example structure:

```json
{
  "partners": [
    {
      "name": "Housecall Pro",
      "commission": "$350/qualified lead",
      "url": "https://housecallpro.partnerlinks.io/your-ref-code",
      "keywords": ["field service software", "plumbing software", "scheduling app contractors"],
      "tracks": ["guides"],
      "context": "Field service management for home service businesses."
    }
  ],
  "disclosure_text": "*Some links in this post are affiliate links. We may earn a commission at no extra cost to you.*"
}
```

The Affiliate Linker reads this, scans the draft for keyword matches, and inserts links naturally. If you do not have affiliate partners yet, you can leave the partners array empty and the Affiliate Linker becomes a no-op.

### published.json

Registry of every post the engine has shipped. Used to prevent duplicates and to check internal linking. Captain appends new entries here. Initial state on a new install:

```json
{
  "posts": [],
  "last_updated": "2026-04-17T00:00:00Z"
}
```

If you have existing blog posts, seed them here so the Scout does not propose duplicate topics.

### The MDX validator

Write a Node.js script at `scripts/validate-mdx.js` that uses `gray-matter` to parse frontmatter and check rules: title under 70 chars, excerpt 140-170 chars, date in YYYY-MM-DD, valid category, no em-dashes in body, affiliate disclosure present if affiliate links detected.

We wrote ours with vitest tests covering 11 cases. The validator is what the SEO Auditor calls and what the pre-push hook calls. Bake your business rules into it.

## 5. Phase 2: the writing skill

This is the part that determines whether the system produces useful content or generic slop. We rewrote the writing skill three times. The version that worked was not about adding voice. It was about listing what not to do, with specific examples.

### What does not work

Telling the model "sound human" produces over-vivid scene-setting and manufactured casualness. Examples from our first generated post:

- Opening: "Your phone rings while you're elbow-deep in a burst pipe under someone's kitchen sink."
- Hedging: "The numbers are rough."
- Metaphor: "Housecall Pro is your back office. The AI receptionist is your front door."
- Authenticity signal: "Honestly, if you're a one-person operation..."

Every one of those is what an LLM produces when asked to "be more human." Real human writers do not open with vivid scenes designed to hook the reader. They state the point.

[VISUAL 3: Before/after voice comparison. "elbow-deep in a burst pipe" paragraph vs edited version]

### What works

Tell the model what NOT to do, with specific examples. Our writing skill has these sections:

**No hedging.** Cut "honestly", "to be fair", "let's be real", "it's worth noting", "arguably". State the claim or cut it.

**No manufactured metaphors.** Just explain what each thing does.

**No inflated findings.** A pricing comparison is a pricing comparison, not "what we discovered" or "a key insight". If the finding is interesting, the reader will notice without being told.

**No tautologies disguised as insights.** Cut anything where the conclusion is already in the premise. "The key to prospecting is implementing strategies that find prospects" is meaningless.

**Specific banned vocabulary.** We have a table:

| Kill | Use |
|------|-----|
| delve into / deep dive | look at, just start talking |
| leverage (verb) | use |
| landscape (metaphorical) | name the specific domain |
| navigate (metaphorical) | deal with, handle |
| robust | strong, reliable, describe what makes it so |
| seamlessly | describe what actually happens |
| holistic / comprehensive | be specific about what's covered |
| game-changer / revolutionize | state the measurable difference |
| In today's [X] | cut, start with the actual point |
| This is where X comes in | cut, just introduce X |
| Let's explore / unpack | cut, just do it |
| You might be wondering | cut, nobody was wondering |

**No em-dashes.** Maximum 1-2 in an entire piece. Default to commas, periods, parentheses, or splitting into two sentences. Em-dashes are the single biggest AI tell. Our pre-push hook physically rejects posts with em-dashes in the body.

**Uneven paragraph rhythm.** A two-sentence paragraph next to a seven-sentence one, because that is where the thought lives. Uniform paragraph length is an AI signature.

**Specifics over vagueness.** Name the tool, version, city, number. "Sarvam AI at ₹1/min" beats "an affordable voice solution". Vagueness is the most common AI tell.

**Show the failed attempt.** Real writers admit when something did not work. AI defaults to "everything we tried succeeded". Include the version that failed.

**End when the thought is done.** No motivational call to action. No "the future is bright". If the post is over, stop writing.

When we ran the same topic through the Writer agent before and after these rule additions, the output transformed. Same model, same topic, completely different feel. The second version reads like a person wrote it because it stopped trying to perform humanness.

### The track-specific templates

On top of the base writing skill, we have a separate template for each publishing track. The templates set:

- Voice (first-person plural for technical, second-person for guides, hybrid for product)
- Word count (2000-5000 for technical deep-dives, 800-1500 for guides, 1000-2000 for product)
- Required components (StatHighlight, Callout, Comparison, PullQuote)
- Citation requirements
- Affiliate link rules (zero for technical track, max 3 for guides, none for product blog)

Each Writer or Rewriter run loads the base writing skill plus the relevant track template. The agent knows what voice to use because the prompt says which track this row is for.

## 6. Phase 3: the seven agents

The agents are the operational layer. Two of them (Scout, Captain) are scheduled. Five of them (Writer, Rewriter, Creative, Affiliate Linker, SEO Auditor, Publisher) are spawned by Captain on demand.

### Scout

Runs once a week. Its job is to extend the planner, not to originate it. The originating topics came from the insight extraction pass we did before launch (Section 4). Scout takes those themes and the posts that have shipped since, looks for adjacent keyword gaps, and adds fresh rows. Its prompt:

- Reads the planner and published registry to avoid duplicates
- Researches keyword gaps across three clusters (one per track)
- Cross-references with affiliates.json to find high-value keyword + partner matches
- Adds 10-15 new rows to planner.csv
- Commits and pushes the planner update

The track assignment rules in Scout's prompt are strict because a misassigned topic produces wrong-voice content. We learned this the hard way when an early Scout tagged "AI for plumbers" as journal track instead of guides. The Writer produced a 3000-word technical post about something the audience did not want.

If your seed rows dry up (Scout is piling topics the Captain cannot prioritize, or the queue has drifted away from what you actually work on), the fix is to rerun the insight extraction pass against recent material, not to crank Scout harder. Scout amplifies whatever it inherits.

### Captain

Runs daily. Its prompt:

- Reads the planner and picks 1-2 highest-priority queued rows
- For each row, spawns the appropriate Writer or Rewriter
- Then spawns Creative on the draft
- Then spawns Affiliate Linker if the track allows
- Then spawns SEO Auditor
- If audit passes, spawns Publisher
- Updates planner status throughout

Captain is the only agent that orchestrates other agents. The five sub-agents below run inside Captain's session as Agent tool invocations.

### Writer

Produces new MDX from a topic. Loads the base writing skill plus the track template. Researches the topic via WebSearch and Brightdata. Writes the full MDX file with frontmatter. Saves to /tmp.

Critical instruction: write specifically. Name tools, versions, prices, cities. Do not produce vague summaries.

### Rewriter

Takes an intern draft (or any source .docx) and rewrites it from zero in our voice. The key rule: extract the research, discard the prose. The intern content is a research input, not a draft to polish.

Rewriter workflow:
1. Extract every specific data point worth keeping (numbers, named tools, citations)
2. Fact-check the data points via WebSearch
3. Restructure to match the target track template
4. Write from scratch using the verified data as evidence

The output should not feel related to the source. If a reader compared both, they should think two different people wrote them.

### Creative

Reviews the draft and decides whether to add visual components. This always runs but does not always make changes. If the Writer already placed sufficient components, Creative reports "no changes needed" and exits.

When Creative does add something, it picks one specific gap. A pricing comparison without a `<Comparison>` component. A high-stakes argument without a `<PullQuote>`. An architecture explanation that needs a Mermaid diagram.

We made Creative mandatory rather than optional because review-passes catch what the Writer misses. The cost of Creative running and finding nothing is low. The cost of a post shipping without proper visual hierarchy is higher.

### Affiliate Linker

Skip this section if you do not have affiliate partners.

Reads affiliates.json. Scans the draft for natural placement points where partner keywords appear. Inserts links on the first meaningful mention only, not every mention. Adds the disclosure text from affiliates.json before the first affiliate link.

Hard rules: never open a section with an affiliate link. Maximum 3 affiliate links per post. Do not put affiliate links in comparison table cells.

The output is honest. If a tool has weaknesses, the post should say so plainly. "Smith.ai gets expensive past 30 calls" is the kind of qualifier that makes the post trustworthy. Affiliate disclosure plus honest assessment converts better than uniformly positive recommendations.

### SEO Auditor

The validation gate. Runs every check in our SEO audit skill:

- Frontmatter validation (title length, excerpt length, valid category)
- Structure validation (first H2 within 300 words, H2/H3 hierarchy correct)
- Content quality (target keyword in first 100 words, density 1-2%, internal link present, external link present, MDX component used)
- Em-dash check (hard fail if found)
- Affiliate compliance (disclosure present if links detected)
- SERP competition check via Brightdata

Reports PASS or FAIL with specific issues. If FAIL, Captain logs the issues in the planner notes and does not publish.

The em-dash rule is the most aggressive one. Em-dashes are technically valid punctuation, but they are also the strongest single AI tell. We chose to forbid them entirely rather than try to police usage. The Writer and Rewriter know this rule, so they do not produce them in the first place.

### Publisher

Executes the actual publish:

- Copies the MDX file from /tmp into src/content/blog/
- Updates published.json
- Validates one more time via validate-mdx.js
- For auto-publish tracks: git add, commit, push to main
- For PR-gated tracks: create branch, commit, push, gh pr create
- For auto-publish: notify IndexNow on the URL

The IndexNow notification matters. Google does not officially use IndexNow but watches it. Bing, Yandex, Naver, and Seznam do use it. Pinging IndexNow on every publish gets your post into Bing's index within hours instead of weeks.

## 7. Phase 4: SEO essentials that actually move the needle

The agents handle content. Your Next.js site needs to handle technical SEO. If you skip this, your posts will not get indexed regardless of how good they are.

### Canonical URLs on every blog post

In your `src/app/journal/[slug]/page.tsx` (or whatever route handles your posts), the generateMetadata function must set alternates.canonical to the absolute URL.

```typescript
export async function generateMetadata({ params }) {
  const post = getPostBySlug(slug);
  return {
    title: `${post.metadata.title} | Your Brand`,
    description: post.metadata.excerpt,
    alternates: {
      canonical: `https://yoursite.com/journal/${slug}`,
    },
    openGraph: { ... },
  };
}
```

Without canonical URLs, Google may see duplicate content from different access paths and decide to index none of them. We had this exact problem on our older posts before we added canonicals.

### Article JSON-LD schema

Every blog post needs a `<script type="application/ld+json">` block emitting Article schema. This tells Google "this is an article" rather than leaving it to guess from the page structure.

We built an `ArticleSchema` component that takes title, excerpt, slug, date, author, category as props and emits the full JSON-LD with publisher info. Drop it into the blog post page right at the top of the JSX.

### Dynamic sitemap

In Next.js 13+, create `src/app/sitemap.ts` that returns a `MetadataRoute.Sitemap`. Read your published posts via the same helper your blog listing uses. Include all blog posts with lastModified dates. The sitemap auto-updates when you publish new posts because it reads from the file system.

### IndexNow integration

Get an IndexNow key from indexnow.org or generate your own (we use a custom string). Host it as a text file at the root of your domain: `https://yoursite.com/{key}.txt` containing only the key.

Write a script `scripts/notify-indexnow.js` that POSTs to api.indexnow.org with your key and a list of URLs. Captain runs this after every publish.

This is the only meaningful difference between getting indexed in days versus weeks.

### Submit your sitemap to Google Search Console

One-time action. Go to search.google.com/search-console, select your property, click Indexing → Sitemaps, paste `sitemap.xml`. Google fetches it within minutes and starts crawling.

After the initial submission, Google re-crawls the sitemap on its own schedule. You do not need to resubmit when new posts go live (assuming your sitemap is dynamic).

### Internal linking

The SEO Auditor enforces "at least one internal link to an existing published post". This rule matters more than people think. Internal links signal topical relationships and accelerate indexing for new posts because Google discovers them by crawling links from already-indexed pages.

We do not have automated cross-linking yet (when a new post publishes, automatically add a link to it from older relevant posts). It is the next thing we want to build.

## 8. Phase 5: the safety hook

The agents have git push access to your repo. If something goes wrong with the prompt or the model misbehaves, you do not want them pushing code changes to main, modifying your settings, or deleting files.

The pre-push hook physically rejects bad pushes. It is enforced by git itself, not by the agent prompt, so even if the prompt drifts, the push fails at the git layer.

### How it works

Create `.githooks/pre-push` in your repo with a bash script that:

1. Reads the refs being pushed
2. Skips anything that is not pushing to main
3. For pushes to main, runs `git diff --name-only` to see what files changed
4. Checks each changed file against an allowlist regex
5. If any file is outside the allowlist, prints an error and exits with code 1

Our allowlist for the your-main-repo repo:

```bash
ALLOWED='^(src/content/blog/[^/]+\.mdx|\content-engine/(planner\.csv|published\.json)|public/og-images/.+|public/blog-images/.+)$'
```

For the secondary repo (Nicherly), even tighter:

```bash
ALLOWED='^src/content/blog/[^/]+\.mdx$'
```

### How to activate it

Git's `core.hooksPath` is per-clone, not committed to the repo. So the hook lives in `.githooks/pre-push` (committed), and you activate it per-clone with `git config core.hooksPath .githooks`.

We wrote a one-line script `scripts/setup-hooks.sh` that runs that config command. In the Actions setup, run it as a workflow step right after checkout, before the agent step starts, so the hook is active for everything the agent does in that job.

On your local clone, you do not run setup-hooks.sh unless you want the restriction. Most days you push code changes, so leaving the hook inactive locally is correct. The workflow activates it because the agent should never push code.

### What if the hook blocks a legitimate push

Two scenarios:

1. The agent tried to push something it should not have. This is the hook doing its job. Read the rejected file list in the workflow logs, fix the prompt or the planner state.

2. You manually need to push something to main from a clone where the hook is active. Use `git push --no-verify` once. This is the only valid use of `--no-verify` for human pushes.

The agent should never use `--no-verify`. We put this rule in the Captain prompt explicitly.

## 9. Phase 6: GitHub Actions setup

> **Second edition note.** The first edition of this book set the schedulers up as Claude Code Routines, then in research preview. We ran that architecture for three months, then moved everything to GitHub Actions in July 2026 after the two systems ended up running side by side and double-publishing (the full story is gotcha #2). Everything below is the Actions architecture the engine runs on today. It is simpler than the routines version: no cloud environment config, no promotion workflow, no branch restrictions.

The whole scheduling layer is one workflow file plus one reusable writer workflow. The agents themselves run through `anthropics/claude-code-action`, the official GitHub Action that starts a Claude Code session on the runner with a prompt you supply.

### Pre-requisite: one OAuth token and two secrets

Generate a Claude Code OAuth token from your subscription by running `claude setup-token` locally. Add it to the repo as a secret named `CLAUDE_CODE_OAUTH_TOKEN`. That is the entire auth story: no API key, no separate billing. Runs draw from the same usage pool as your interactive sessions.

We also store `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` as secrets so every job can ping a Telegram chat when something fails. Any notification channel works; the point is that an unattended engine needs a failure channel you actually look at.

### The prompts live in the repo

Each agent's prompt is a markdown file under `content-engine/prompts/`: `scout.md`, `captain.md`, `writer.md`, `rewriter.md`, `post-mortem.md`, and the rest. The workflow reads the file and passes it to the action verbatim. This matters more than it looks: prompts in the repo are versioned, diffable, and editable from any machine, which the routines-era web UI textboxes were not.

### The daily workflow

`.github/workflows/content-engine-daily.yml` has two cron triggers and four jobs:

```yaml
on:
  schedule:
    - cron: '13 6 * * 1,4'         # Mon + Thu: scout + captain + writers
    - cron: '13 6 * * 0,2,3,5,6'   # other days: captain + writers only
  workflow_dispatch:
    inputs:
      run_scout: {description: "Run Scout step", type: boolean, default: false}

permissions:
  contents: write
  pull-requests: write

concurrency:
  group: content-engine-daily
  cancel-in-progress: false
```

Pick a cron minute that is not :00. Everyone schedules on the hour, GitHub delays those runs the most.

**Job 1: scout** runs only on the Mon + Thu cron (the job's `if:` checks which schedule fired). It loads `content-engine/prompts/scout.md` and hands it to the action:

```yaml
- uses: anthropics/claude-code-action@v1
  with:
    claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
    prompt: ${{ steps.load.outputs.prompt }}
    claude_args: '--allowedTools "Read,Write,Edit,Bash,WebSearch,WebFetch,Glob,Grep"'
```

`--allowedTools` is the permission boundary. Scout gets WebSearch and WebFetch because research is its job. Captain does not.

**Job 2: captain** runs daily, after scout when scout ran. Captain reads the planner and picks the day's topics, but it does not write posts itself anymore. It writes its decision to `/tmp/captain-decision.json`, and a plain shell step parses that with `jq` into job outputs (slug, track, title, keyword). This is the pattern worth stealing: let the LLM make the judgment call, then hand the result to deterministic YAML instead of trusting one long agent session to do everything.

**Jobs 3: writers** are a reusable workflow (`writer.yml`, `workflow_call`) invoked once per picked topic with the slug, track, and title as inputs. Each writer job checks out the repo, runs the Writer or Rewriter prompt, then Creative, Affiliate Linker, and SEO Auditor, and publishes per the track's rules. Writer jobs for different sites run serialized, not parallel, because two agents pushing planner updates at the same time is a race you will lose. The `workflow_dispatch` trigger on `writer.yml` doubles as a manual "write this one post now" button.

**Job 4: post-mortem** runs `if: always()` after everything else. It gets each job's result injected into its prompt, reads the day's logs and diffs, and sends a Telegram summary of what published, what failed, and why. A failsafe job behind it pings Telegram if anything upstream failed or got cancelled, so a silent runner-queue failure still surfaces.

### Publishing rules per track

Auto-publish tracks (guides, product blog) push straight to main with the workflow's `GITHUB_TOKEN`; the validator and the pre-push allowlist are the gate. The journal track opens a PR instead, and a human merges it. A separate small workflow (`content-reconcile.yml`) watches main for merged journal posts, registers them in `published.json` and the planner, and pings IndexNow. Pushes made with `GITHUB_TOKEN` do not trigger other workflows, which is exactly what stops the engine's own pushes from re-triggering the reconcile loop.

### Verify with a manual run

Trigger the workflow from the Actions tab with `workflow_dispatch` (tick `run_scout` if you want the full pass). Watch the live logs. Verify Scout adds rows, Captain writes a decision JSON, the writer produces a draft that survives the SEO audit, and the publish lands where the track's rules say it should. If an agent misbehaves, cancel the run from the UI, fix the prompt file, push, and dispatch again. The edit-to-retry loop is a git commit, not a web form.

### Timeouts and concurrency

Give every agent job a `timeout-minutes` (ours: 30 for scout/captain, 90 for writers). An agent that wedges on a broken tool call will otherwise sit on the runner for six hours. The `concurrency` group with `cancel-in-progress: false` stops a slow Monday run and Tuesday's run from operating on the planner at the same time.

## 10. Phase 7: integrating with a secondary site (skip if you only have one)

If you only have one site, your setup ends at Phase 6. Skip this section.

If you have a primary brand site plus a secondary site (in our case, a SaaS product blog on a separate domain), you can have the engine publish to both repos. The architecture extends like this:

- The writer workflow takes a `site` input; when it targets the secondary site, an extra `actions/checkout` step clones that repo into a subdirectory using a deploy key stored as a secret
- Captain's decision JSON carries a slot per site, so one daily run can pick a primary topic and a secondary topic
- The pre-push hook in the secondary repo has its own (typically tighter) allowlist
- The published.json registry stays in the primary repo and tracks posts from both sites (so Scout does not propose duplicates)

The writer agent switches into the secondary checkout with `cd`, publishes there, then returns to the primary repo to update the planner. Writer jobs for the two sites run serialized so the planner never sees two concurrent pushes.

The secondary repo needs its own writing skill (the voice may differ from the primary brand) committed at `.claude/skills/`. Captain reads it during Writer/Rewriter spawns for that track.

This adds setup complexity but lets you run two brand voices through one engine.

## 11. What bit us

[VISUAL 5: What-bit-us map. 9 failures organized by stack layer]

Read this section even if you skip everything else. Some of these cost us hours. A few are platform realities you have to design around.

### 1. Agent runs do not pick up `.claude/settings.local.json`

The settings.local.json file is gitignored by default. The workflow's checkout does not include it, so any local permission allowlists you set up on your laptop do not transfer to CI.

Two options: commit `.claude/settings.json` (without `.local`) for permissions you want in CI, or pass `--allowedTools` explicitly in each workflow's `claude_args` and trust that plus the pre-push hook. We went with explicit `--allowedTools` per job. The workflow file is the single place you audit what each agent can do.

### 2. Two schedulers, one engine: the double-publish failure

This replaced the first edition's headline gotcha, and it is a better lesson. The first edition's constraint was that Claude Code Routines could only push to `claude/*` branches, never main, which forced a promotion workflow that watched those branches, validated, and fast-forwarded main. That pattern worked. It worked so well that when we later rebuilt the engine on GitHub Actions, the old routines kept running beside it and nobody noticed.

For a month the engine ran twice. The GitHub Action fired daily, committing as `claude[bot]`. The leftover routine fired weekly on Mondays, committing under a different author name. Every Monday, both scouted the same day and the planner queue inflated with near-duplicate topic rows, which accelerated exactly the saturated-series pileup the quality rules were supposed to prevent. We found it while auditing why the queue mix had drifted, deleted the routines, and made the GitHub Action the sole system of record.

The lesson generalizes: an autonomous engine must have exactly one scheduler, and you should be able to answer "what fires this, and when" with one file in the repo. If you migrate architectures, decommissioning the old one is part of the migration, not cleanup for later. Check your commit authors occasionally; two different bot identities writing to the same planner is the smoking gun.

The happy postscript is that the constraint that forced all the promotion machinery is gone. An Actions workflow pushes to main directly with `GITHUB_TOKEN`, gated by the validator and the allowlist hook. The promotion workflow and the branch gymnastics got deleted along with the routines.

### 3. `<>` in MDX silently breaks Vercel builds

Our first guides-track post had the phrase "Shopify <> ERP" in a section heading. Vercel's build step exploded with a parse error pointing at what looked like valid markdown. MDX treats `<>` as an empty React fragment. Between two regular words in prose, that is nonsense, and the parser rejects the file.

The failure mode is worse than a normal build error because the agent's session already reported "publish complete" before Vercel picked it up. The agent had committed, the push had landed on main, and only then did Vercel turn the deployment red. From the workflow's perspective, the post had shipped. From the reader's perspective, the entire site was broken.

We added an explicit rule to the Captain prompt that the Writer must never emit `<>` between terms anywhere in an MDX body. Use "to," "and," "vs," or escape as `&lt;&gt;`. We also added a check to the validator: if the MDX body contains `<>` outside a fenced code block, it fails before the commit. Stopping the post before the push is better than catching it after the deploy turns red.

### 4. Affiliate detection false positives

Our first affiliate disclosure validator triggered on the literal word "partner" in body text. "Distributor partners" in a supply chain post was flagged as an affiliate link, which then forced the validator to demand an affiliate disclosure for a post with no affiliate links. The post failed audit. The planner marked it `seo_failed`. It sat there for a day before we noticed.

What worked: match URL patterns, not English words. Our current detector looks for `?ref=`, `partnerlinks.io/`, `trial.nextiva.com/`, and the explicit phrase "affiliate link." Plain English does not trigger it.

### 5. The pre-push hook can block its own setup push

The first time we set up the hook, we tried to push the hook itself plus its setup script. The hook rejected the push because those files were outside the allowlist. It is the only situation where bypassing the hook is legitimate: use `git push --no-verify` for the one-time infrastructure push that installs the hook. After that, the agent should never use `--no-verify`, and neither should you.

### 6. Article schema has to render on the server, not the client

We wired up Article JSON-LD on every blog post, deployed, ran the posts through Google's Rich Results Test, and every single one returned "No items detected." The schema was rendering in the DOM but not in the initial HTML. We had imported `MDXRemote` from `next-mdx-remote` (the default entry), which does client-side rendering inside a React component tree. Schema embedded inside a client tree does not land in the server-rendered HTML that the crawler sees on the first pass.

Google's crawlers do execute JavaScript. Some indexing paths rely on the first-pass HTML only. Rich Results validation specifically checks the initial response. If the schema is not there, Google may treat the post as a generic web page instead of an article, and you lose article-specific carousel eligibility in SERPs.

Switching the import to `import { MDXRemote } from 'next-mdx-remote/rsc'` and lifting the `ArticleSchema` component up to the server component that renders the page (not inside the MDX body) solved it in a single deploy. Rich Results Test started returning valid schema for every post. If you can, test the fix against one post before backfilling the import across the repo, because the fix is a one-liner but the debugging round-trip is painful.

### 7. MCPs in CI need explicit wiring (and you may not need them at all)

In the routines era, stdio-based MCPs simply did not work; only hosted connectors did. On a GitHub runner that limitation is gone, since the runner is a full Linux box: pass an MCP config to `claude-code-action` via `--mcp-config` in `claude_args` and stdio servers run fine, with any API keys supplied as repo secrets.

What we actually found is that we stopped bothering for the scheduled runs. WebSearch and WebFetch cover Scout's research pass, and every MCP you wire into CI is another credential to rotate and another startup step that can fail at 6 AM with nobody watching. Add an MCP to the workflow when a specific agent demonstrably needs richer data, not by default.

### 8. CI checkouts use a different working directory than your laptop

The workflow checks your repo out to something like `/home/runner/work/your-repo/your-repo/`. Your local clone is wherever you put it. Hardcoded absolute paths in the prompts will fail silently in CI because the prompt-level path check succeeds locally when you are testing. Use paths relative to the repo root throughout the prompts: `content-engine/planner.csv`, not `/Users/yourname/repo/content-engine/planner.csv`. The same rule covers the handoff files the agents write for the workflow to parse; `/tmp/captain-decision.json` is the one absolute path we allow, because `/tmp` exists on every runner.

### 9. Your existing posts may fail the new validator

When you first wire up the validator, run it against every existing post in `src/content/blog/`. Some will fail. The common issues are excerpt too long, em-dashes throughout, and wrong category values from a schema that has since evolved. You can either fix the existing posts (we did) or grandfather them in. We fixed ours as part of a content quality pass, which was useful on its own because several of the failures turned out to be real typos we had never caught.

## 12. What it costs to run

[VISUAL 7: Cost breakdown. Monthly spend by component]

Real numbers, depending on which plan you pick and which optionals you take:

**Claude subscription — $20 to $100/month.** The Pro plan at $20/month (or $17 on annual billing) is the floor. It includes Claude Code and Cowork (which is what we used for the insight-extraction pass). For our actual run rate (1 Captain per day, 2 Scouts per week, plus interactive development) Pro has been sufficient. The $100/month Max plan gives roughly 5x the usage if you publish more aggressively or use Claude Code heavily for client work on the same account. The CI runs authenticate with an OAuth token generated from the subscription and draw from the same usage pool as your interactive sessions, so the engine and your day job share one quota. If the engine starts starving your interactive work, that is the signal to move up a plan.

**SERP-scraping MCP (optional) — about $20/month.** We use Brightdata at around $20/month of pay-per-call volume for the Scout and SEO Auditor passes. The system runs fine without it on the built-in WebSearch. We added it after the first month when the journal-track research started needing real competitor word counts.

**Domain name — ~$10/month, amortized.** A `.com` or similar runs $10-15/year. Small and easy to forget, but real.

**Voice-to-text tool (optional) — up to $10/month.** If you dump thinking as voice memos to seed the planner, you need something that turns audio into text. Cowork covers this in our setup. If you do not use Cowork, macOS built-in dictation is free, Whisper API costs a few cents per run, and Otter or Rev is $10-17/month. Only count it if you are paying for a standalone transcription service.

**Vercel — $0/month.** Free tier handles blog traffic comfortably. Hobby plan ($20/month) is only needed if you push into tens of thousands of monthly visitors, which a young content engine will not.

**GitHub Actions — $0 on a public repo, budget for minutes on a private one.** The scheduler is now Actions, so runner time is a real line item. Agent jobs are not quick: our daily pipeline (captain plus one or two writers plus post-mortem) can use one to three hours of runner time a day. On a public repo that is free. On a private repo the free tier is 2,000 minutes a month, which a daily engine will exceed; overage is $0.008 per minute, so figure $0-30/month depending on cadence. A self-hosted runner brings it back to zero if you have a box that is always on.

**IndexNow — free.**

**Affiliate partner referral codes — free.** Revenue side, not cost side.

**Total: $20-60/month on Pro, $100-140/month on Max.** The floor is $20 (Pro subscription only). Add $20 for paid SERP, $10 for domain amortized, $10 if you pay for transcription, and you land at $60 on Pro or $140 on Max. That is the full cost of the content engine. Compared to a freelance content writer at $200-400 per post plus a separate SEO consultant, the Pro setup pays for itself after the first publishable post of the month. We are producing 30+ per month at full ramp.

One cost we deliberately did not include is "your time setting it up," because that is a one-time investment. After setup, the recurring burden is reviewing PRs from the journal track and occasionally updating the planner when Scout drifts or when we do another insight-extraction pass against new material.

## 13. Voice rules: the AI tells we found and how to avoid them

This is the section that got the most attention when we shared this internally. If you take one thing from this guide, take this section.

The single biggest tell: AI writing reads like it was written by someone with no stakes in the outcome. It covers all sides, offends no one, commits to nothing. Real writing has a point of view and something to lose.

We ran our first generated post through the system. It looked good on first read. The user who was supposed to approve it (one of us) called out everything that was wrong:

- "Elbow-deep in a burst pipe" was creative writing class energy
- "The numbers are rough" was AI performing casualness
- "Honestly, if you're a one-person operation" had two AI tells in one sentence
- The "front door vs back office" metaphor was manufactured

We were embarrassed because we had built the system and missed all of this on our own read. The output looked human if you were not looking for AI tells. Once you knew what to look for, every paragraph had at least one.

Three patterns showed up in almost every draft we had to rewrite.

The first is structural symmetry. The model opens with a thesis, drops a neat list of three, then restates the thesis at the end. Paragraphs run a uniform three-to-five sentences. Section counts are suspiciously round. The draft moves from section to section with transitions like "now that we've covered X, let's turn to Y." None of this is how people actually write. It is what you get when a model optimizes for sounding like it knew what it was doing.

The second is hedging combined with inflation. The sentence hedges the claim (arguably, to some extent, it's worth noting) and then inflates the finding (a critical insight, a game-changer, a breakthrough). The net effect is prose that commits to nothing but tries to sound important anyway. Real writers either state the claim or admit they are not sure. They do not do both in the same sentence.

The third is uniform pleasantness. Every section ends on an upbeat note. Nothing in the draft is hard, ugly, or unresolved. There is no "we tried this and it did not work." There is no "we do not know why this happens." The draft reads as if the author had no stakes. Real writing about real work includes friction, and the friction is how readers trust the rest.

The vocabulary tells are narrower and easier to fix. Here is the table we built into the writing skill:

[VISUAL 4: AI tells typographic poster. Kill this / use this pairs]

| Kill | Use instead |
|------|-------------|
| delve into / deep dive | look at, just start |
| leverage (verb) | use |
| landscape (metaphorical) | name the specific domain |
| navigate (metaphorical) | deal with, handle |
| robust | strong, reliable |
| seamlessly | describe what happens |
| holistic / comprehensive | be specific |
| game-changer / revolutionize | state the measurable difference |
| harness the power of | use |
| In today's [X] | cut, start with the point |
| This is where X comes in | cut, just introduce X |
| Let's explore / unpack | cut, just do it |
| You might be wondering | cut, nobody was |

The fix is not to "add personality." It is to stop removing the rough edges that make writing trustworthy. Tell the model what NOT to do, with specific examples. The output will change on the next run.

## 14. What we would do differently

If we were starting over, two changes would have saved us a week.

We would launch with one publishing track, not three. We tried to ship journal, guides, and nicherly simultaneously, and each had a slightly different voice, structure, and quality bar. Tuning across three dimensions in parallel meant every voice-rule tweak had to be validated against three different output types. Picking the most templated track first (guides, in our case) and getting it right before writing a journal post would have compressed the tuning cycle by at least half. The other tracks inherit most of the work once one is solid.

We would also build the voice rules before any agent infrastructure. We spent the first day on architecture (planner, agents, orchestration) and the third day rewriting the writing skill after the output came back as slop. Inverting that order would have meant the agents shipped into a voice system that already worked. Architecture is cheap to change. The voice rules took real iteration, and every iteration required regenerating test posts. Build the part that needs the most iteration first.

## 15. Closing: about South Arc Digital

We are a small AI automation studio run by two founders: Vignesh Ramakrishnan (vignesh@southarcdigital.com) and Ram Kowshik (ram@southarcdigital.com). We build custom integration software for e-commerce operations, manufacturing, and inventory-heavy businesses. Most of our work involves bridging legacy software with modern AI for clients who would otherwise stay on manual processes.

We also run Nicherly, a product for AI automation agency owners. It helps them find local business prospects through Google Business Profile audits, replacing manual prospecting work that otherwise eats their week.

The content engine serves both businesses at once: the technical track builds credibility with the e-commerce operations buyers South Arc sells to, and the same pipeline publishes affiliate-monetized guides and Nicherly product posts without a second setup. One engine, three voices, one planner, one set of voice rules.

If you want to read the actual posts the engine has produced, they are at https://southarcdigital.com/journal.

If you want help building something similar, you can reach us at https://southarcdigital.com/contact. We do not currently sell content engine setup as a service, but we do offer custom integration work for businesses that need it.

If you build your own version of this and it works, we would like to know. Reach out via the contact page above.

---

## A note on how this was written

The goal wasn't AI that sounds human. It was a system for AI to create rankable content. Four things in sequence: (1) reads your own thinking, (2) brings it up to date with current research, (3) puts it together against an editorial and SEO bar with real visuals, and (4) publishes it.

The topics in this book came from us, not from a keyword tool. We ran our meeting transcripts, voice memos, and internal Slack threads through Cowork to extract the themes we had actually been working through. Those themes seeded the planner. Scout added current SERP data, recent pricing, and named tools. Writer drafted against our voice skill. SEO Auditor ran the full audit. Nothing shipped until it passed.

This book went through a simpler version of the same loop. We dumped our build notes, internal threads, and post-mortem voice memos into a Claude instance (not the content engine itself, a separate session). It produced a first draft. We then spent several edit passes on what the model could not get right from the raw input. Five specific tells we caught and cut:

- "The third version was the breakthrough." Inflation of an ordinary thing. A skill we rewrote twice is a skill we rewrote twice; it is not a breakthrough.
- "That is the whole system. Two scheduled agents, five sub-agents, three data files, five skills, one safety hook. Everything else is glue." An AI recap sentence that restated what the section above already said. Cut the whole thing.
- A tidy three-part taxonomy (Structural / Vocabulary / Tonal) imposed on the voice-rules section. Neat symmetry applied to something messy. Rewrote as three prose paragraphs.
- Five equally-weighted "we would do differently" bullets. Symmetric list standing in for actual judgment. Cut to the two things that actually mattered.
- A triple parallel at the close ("technical builds X, guides drives Y, product drives Z"). Parallel structure as a substitute for a single real claim. Collapsed to one sentence.

[VISUAL 6: Edit-pass stats. Before/after counts]

The thesis is not that AI writing is fine if you edit enough. It is that the editing pass is most of the work, and most AI content is bad because the pass does not happen. The combination of your own thinking, real research on top of it, and honest editing produces content that holds up. Any step you skip is where the genericness enters. If this guide held your attention, those three steps did the work. If it did not, tell us which sections and we will fix them.

---

## Reference bundle

The full working versions of every prompt, skill, script, and template in this book ship as a separate bundle. The bundle is structured to drop straight into a Next.js repo: prompts go under `content-engine/prompts/` where the workflows read them, skills go under `.claude/skills/`, workflow YAML goes under `.github/workflows/`, scripts go under `scripts/` and `.githooks/`.

```
content-engine-bundle/
├── README.md
├── prompts/
│   ├── scout.md                    twice-weekly topic-research agent
│   ├── captain.md                  daily publishing orchestrator
│   ├── writer.md                   new-post sub-agent
│   ├── rewriter.md                 transforms intern drafts and source material
│   ├── creative.md                 visual-component reviewer
│   ├── affiliate-linker.md         affiliate placement and disclosure
│   ├── seo-auditor.md              pre-publish validation
│   ├── reviser.md                  one-shot targeted fixes when the audit fails
│   ├── post-mortem.md              end-of-run planner cleanup + Telegram summary
│   ├── publisher.md                per-track publish rules
│   └── bootstrap.md                one-shot prompt for scaffolding the whole system
├── skills/
│   ├── writing-voice.md            brand-voice writing skill (the most important file)
│   ├── seo-audit.md                full validation checklist
│   ├── guides-template.md          track template for SEO + affiliate content
│   ├── secondary-site-example.md   writing skill for a product blog (example)
│   └── content-rewriter.md         workflow for transforming source material into brand voice
├── scripts/
│   ├── pre-push-hook.sh            safety hook that rejects non-content pushes
│   ├── setup-hooks.sh              activates the hook per-clone
│   ├── notify-indexnow.js          POSTs published URLs to IndexNow
│   ├── content-engine-daily.yml    GitHub Actions workflow: scout + captain + writers + post-mortem
│   └── writer.yml                  reusable per-post writer workflow (workflow_call + manual dispatch)
└── reference/
    └── copy-paste.md               regexes, commands, CSV headers, optional helpers
```

### What to customize before using any of this

The prompts assume two repositories (a primary site plus an optional secondary site). If you run one site, delete the `cd ../secondary-repo` logic in `captain.md` and the secondary-site section of `scout.md`. The skill files contain brand-specific examples throughout; swap those plus the author name for your own. If you have no affiliate program, leave the `affiliates.json` partners array empty and the Affiliate Linker becomes a no-op.

### The prompts

**`prompts/scout.md`**: reads the planner and published registry, researches keyword gaps across your tracks, cross-references `affiliates.json`, and adds 10-15 rows to `planner.csv`. The track-assignment rules at the top are strict. Swap them for your track definitions before first run.

**`prompts/captain.md`**: reads the planner, picks the top 1-2 queued rows, spawns Writer or Rewriter, then Creative, then Affiliate Linker (if track allows), then SEO Auditor, then Publisher. Updates planner throughout. The per-track publishing routing (auto-publish vs PR-for-review) is defined in the middle of the prompt; adjust to match your risk tolerance.

**`prompts/writer.md`**, **`rewriter.md`**, **`creative.md`**, **`affiliate-linker.md`**, **`seo-auditor.md`**, **`reviser.md`**, **`post-mortem.md`**, **`publisher.md`**: the per-step prompts the workflows load. Substitute paths and brand names. The writer and rewriter both load the writing skill and track template at the top of their run, so most customization lives in the skill files rather than these prompts. Reviser gets exactly one attempt at fixing a failed audit; post-mortem closes every daily run with planner cleanup and the Telegram summary.

**`prompts/bootstrap.md`**: paste into a fresh Claude Code session to scaffold the data files, skills, validator, hook, scripts, and workflow YAML in one pass on top of an existing Next.js repo. The agent does the infrastructure work; you still add the repository secrets (`CLAUDE_CODE_OAUTH_TOKEN`, Telegram) in the GitHub settings UI afterward, because secrets should never pass through an agent session. Expect about four hours from scaffold start to first publishable post.

### The skills

**`skills/writing-voice.md`**: the voice rules, the AI Writing Tells section, and the structure template. This is the single most important file in the system. Keep the voice tells verbatim. Swap the author name, the brand-specific examples, and the domain-specific context.

**`skills/seo-audit.md`**: frontmatter validation, structure validation, keyword density, internal and external link checks, em-dash check, word count per track. Used by the SEO Auditor sub-agent on every publish and (in spirit) by the pre-push hook. Change the allowed category list and the word-count ranges for your tracks.

**`skills/guides-template.md`**: track template for SEO-optimized commodity content. Second person, comparison-focused, affiliate-monetizable. Skip if you run a journal-only setup.

**`skills/secondary-site-example.md`**: a second writing skill calibrated for a product blog rather than a technical authority blog. Looser hedging rules, persona-style labels allowed, soft CTA at close. Use as a starting point for a second voice; adapt audience, tone, and CTA rules.

**`skills/content-rewriter.md`**: extract research and discard prose, fact-check the data points, restructure to match the target track, write from scratch. Used by the Rewriter sub-agent when a planner row has `content_type = "rewrite"`.

### The scripts

**`scripts/pre-push-hook.sh`** and **`scripts/setup-hooks.sh`**: the pre-push allowlist and its activation script. Edit the allowlist regex at the top of `pre-push-hook.sh` to match your repo's content paths.

**`scripts/notify-indexnow.js`**: Node.js script that POSTs published URLs to api.indexnow.org. Generate an IndexNow key, host it at `public/{key}.txt` on your domain, and hardcode the key at the top of the script.

**`scripts/content-engine-daily.yml`** and **`scripts/writer.yml`**: the scheduling layer. Drop both at `.github/workflows/`. The daily workflow runs Scout on its cron days, Captain every day, hands Captain's picks to the writer workflow, and closes with the post-mortem job and Telegram failure pings. The writer workflow is `workflow_call` plus `workflow_dispatch`, so it doubles as a manual "write this one post" button. Swap the cron times, track names, and site inputs for yours, and add the secrets before the first run (see Section 9).

### The copy-paste reference

**`reference/copy-paste.md`**: the pre-push allowlist regexes (primary and secondary variants), the validator category list, the IndexNow notify command, a seed planner-row template, and (under an "optional / machine-specific" heading at the bottom) the `chpwd` shell function for auto-syncing repos when you `cd` into them locally.


---

*Vignesh Ramakrishnan and Ram Kowshik are the co-founders of South Arc Digital. Former lawyer and engineering leader turned AI engineers. They build for non-tech businesses that need to bridge old systems with new AI capabilities. South Arc has shipped to four production clients across textiles, industrial gas, FMCG distribution, and legal analytics. Nicherly is the studio's first product, targeting AI automation agency owners. Both businesses run on the content engine described in this guide. Reach out at vignesh@southarcdigital.com or ram@southarcdigital.com.*
