ashrivastava
menu›
// post — career & engineering← all posts

I Turned Off My AI Blog

For three and a half months a GitHub Action published an AI-written post under my name three times a week. What it produced, what I patched instead of questioning, and why 43 of 48 posts are now unpublished.

Amit Shrivastava·14 Sept 2026·7 min read
// body

What I built

On 2026-03-29 I added a blog to this site, and in the same commit (ded03c4) a GitHub Actions workflow to fill it. The setup: a queue of topics in content/blog-topics.json and a cron schedule of 0 9 * * 1,3,5, which is Monday, Wednesday and Friday at 09:00 UTC. The same day, the workflow moved from the Anthropic API to OpenRouter and then to google/gemini-2.5-flash (67dafc0, a6a980e).

Each run took the next queued topic, sent a prompt to the model, wrapped the reply in frontmatter with published: true, and pushed it straight to main. There was no pull request and no review. The first bot post landed on 2026-03-30. The last one, on 2026-07-13, was the final topic in the queue. By then github-actions[bot] had made 44 blog: auto-publish commits. Together with four seed posts, the blog had 48 articles, and I had written none of them.

This week I removed the schedule and unpublished 43 of them.

The prompt was the bug

This is the part of the prompt that mattered:

Author: Amit Shrivastava (Senior Software Engineer with 10+ years experience in Frontend, Web3, and AI)
...
- Be practical and actionable, not just theoretical
- Write in first person from Amit's perspective
- End with a call to action to connect on LinkedIn (...) or X (...)

It asked for first-person experience from a model that has none of mine, so the model made it up. 29 of the 48 posts contain some version of "As a senior software engineer with a decade of experience…". Five open with "Hey everyone, Amit here!". The Computer Use post, whose body was regenerated after a truncation, says "I've shipped enough of these to know the failure modes by heart." I never shipped a computer-use agent. Others described production eval harnesses, internal AI platforms and cost reductions that don't exist anywhere in my work.

None of it was malicious, and it was worse because of that. The posts were fluent and confident, and they sat on a site whose whole purpose is to tell people what I have actually done. A reader had no way to tell the invented parts from the real ones, and I had given them no reason to try.

The repetition gave it away as well. Same opener, same shape, and the same LinkedIn/X call to action at the end of 43 posts.

What I fixed instead

Reading the history back, the uncomfortable pattern is how much effort went into making the pipeline more reliable, and how little into asking whether it should run. In date order:

  • 2026-04-14, 0fe523c. The prompt said "connect on LinkedIn or X" without the URLs. The model filled in made-up placeholder handles, and one post shipped with them in place of my real profiles. The fix put the real URLs in the prompt.
  • 2026-05-07, 4c4d9f2 and 3f79136. A generation hit the token limit, and the bot committed a post that was just frontmatter and an opening fragment. The fixes restored the body, made the workflow abort on finish_reason === 'length', raised max_tokens to 8192, and added scripts/validate-blog-posts.mjs. That validator checks required fields, minimum length, whether the body ends on terminal punctuation, and known placeholder strings.
  • 2026-05-18, a run of commits from ddab949 to 40ebf30. I added a "Code in this post was tested" badge. More on that below.
  • 2026-05-28, 784da2c. Topic titles were interpolated into single-quoted shell strings with '${{ }}'. An apostrophe in a title closed the quote early, cut the topic JSON short and ran the rest as shell commands (exit 127). The fix reads topic fields from a JSON file through Node. Same day, 38d38da: the validator flagged a post ending in _italic_ as truncated, so _ was added to the allowed ending characters.
  • 2026-08-27, 96db05c and b502fdf. Six posts showed a Mermaid error instead of a diagram. The cause was 14 node labels with unquoted parentheses. Another post had its entire article inside a markdown code fence, so the page showed it as one big code block.

Every one of those is a reasonable fix. Most of them were written in Claude Code sessions, as the commit authors show. That makes the whole thing look more like a machine maintaining a machine. None of the fixes touched the actual problem: every post said things about me that weren't true.

The badge that tested the wrong thing

The verification feature is the part I'm least proud of, because it looked like rigor.

scripts/generate-verification.mjs sent the finished post back to the model and asked for a "self-contained Node.js snippet" based on the post's code. It ran the snippet twice, and if the output matched both times, it embedded the snippet and its stdout in the frontmatter. scripts/verify-blog-code.mjs then ran on every PR and on pushes to main, and failed if the output ever changed. The post showed a "Code in this post was tested" badge.

Look at what that actually checks. The model wrote the snippet, the model's snippet produced the output, and CI confirmed the output hadn't changed since the model produced it. It never checked that the post's own code was correct.

The first version didn't even check that the snippet came from the post. ee67758 says the generator "was producing snippets unrelated to the post … making the 'Code in this post was tested' badge misleading." After the prompt was tightened, the snippets had to come from the post, with TypeScript types stripped so they could run as plain JavaScript. For a post about TypeScript, stripping the types strips out the point.

The TypeScript at Scale post carried a passing badge whose snippet does this:

class UserIdBrand {
}
class ProductIdBrand {
}
// ...
console.log("Attempting to call getProductDetails with a UserId (runtime result):");
getProductDetails(myUserId);

It printed Fetching details for Product: user-123, which is the exact bug branded types exist to prevent, and the badge said "passed". Two more fixes followed: 40ebf30 because a snippet wrote its error case to stderr and the recorded output looked cut off, and 2f9a79d, which removed one badge instead of showing that. At its peak, 27 posts had a verification block.

What I kept

Five posts are still published:

  • Reviewing AI-Generated Code: A Practical Framework for Senior Engineers
  • Streaming UI: Building Responsive Interfaces on Top of LLM Token Streams
  • Evaluating AI Agents: Building Test Suites for Non-Deterministic Systems
  • Long-Running Agents: State, Recovery, and Resumability Beyond a Single Turn
  • TypeScript at Scale: Patterns That Survive a 500-File Codebase

I kept them because their subjects are close to what I work on, and they read as technical explainers more than invented memoirs. Their verification blocks are gone, and so are wordings like "the eval harness I actually use in production". They are still AI-drafted, and I'm saying that here instead of letting them pass as something else.

The other 43 are set to published: false. They're still in the repo. Deleting them would hide the history this post is about.

The rule now

The workflow file is still there, but the schedule: trigger is removed, with a comment explaining why. The "first person from Amit's perspective" line is replaced by an instruction not to invent employers, projects, incidents, costs or metrics.

The rule going forward is simple: I only write about work you can check. If a post says I built something, the code is in this repo or linked from it. If it quotes a number, the number comes from a commit, a config file or a measurement I can show. The SiteAsk write-up is the first post under that rule. Every figure in it traces to a file or a commit hash.

I'll publish less often. That was the goal from the start. The problem was never that an LLM helped. The problem was a cron job speaking in my voice about things that never happened, and me spending months making it more reliable at doing that.