I run four AI-focused technical blogs as a solo developer, and Claude Code Skills handle most of the daily content generation. Each site publishes around four articles per day, which adds up to roughly sixteen automated pushes every twenty-four hours. The first few weeks felt smooth. Then I started noticing intros that read suspiciously alike across sites, even though my SKILL.md explicitly bans certain template openers.
Tracking down the cause led to a simple realization: natural-language rules alone cannot fully constrain a model that runs that often. Across sixteen runs a day, the wording of a topic, the framing of an example, or just the temperature roll of a particular call lets template phrases sneak through every so often. One article a day might be invisible. A month of accumulated drift across hundreds of articles is not. In my case, this slow accumulation was enough to drag down search visibility on one of the sites noticeably.
What turned that around was almost embarrassingly low-tech. I added grep checks to the final step of my SKILL.md. Prompt rules are requests; bash exit codes are walls.
Why prompt-only constraints leak
Claude Code Skills follow the steps and rules written in SKILL.md. Most of the time, "do not use phrase X" works fine. But it is not deterministic. Out of a hundred generations, a few will slip past, and at sixteen runs a day that adds up quickly.
The patterns I observed in my own production:
- The Japanese intro pattern was avoided in the JA file, but the English version drifted into a familiar opener
- Banning "the complete guide" in titles pushed the model toward "the definitive guide" and other near-paraphrases
- Closing template lines disappeared from one phrasing, but a slightly reworded variant quietly took its place
Reviewing each article by eye would catch these, but at four articles per site per day across four sites, manual review is not realistic. The check has to live inside the Skill itself.
A minimal grep guard for SKILL.md
Here is the section I now keep at the end of Step 4 (quality check) in my SKILL.md. It is short on purpose; a long list of regex would be brittle and noisy. Pulling the patterns into shell variables also makes them easy to extend later without touching the control flow.
# End-of-step guard: refuse to proceed if forbidden patterns appear.
NEW_FILE="$WORK/content/articles/en/${CATEGORY}/${SLUG}.mdx"
VIOLATIONS=0
# (1) Title patterns I never want to ship
TITLE_BAD='(complete guide|definitive guide|ultimate guide|everything you need to know|[0-9]+ best)'
if grep -qiE "^title:.*${TITLE_BAD}" "$NEW_FILE"; then
echo "FAIL: title contains a forbidden pattern"
VIOLATIONS=$((VIOLATIONS+1))
fi
# (2) Opening 600 characters of the body
HEAD=$(awk 'BEGIN{fm=0} /^---$/{fm++; next} fm==2{print}' "$NEW_FILE" | head -c 600)
INTRO_BAD=("Let's dive" "Let's explore" "comprehensive guide")
for phrase in "${INTRO_BAD[@]}"; do
if echo "$HEAD" | grep -qiF "$phrase"; then
echo "FAIL: opening contains \"$phrase\""
VIOLATIONS=$((VIOLATIONS+1))
fi
done
# (3) Conclusion templates
CONCL_BAD='(hope this (was|has been) helpful|youll be able to master|in conclusion, we have)'
if grep -qiE "$CONCL_BAD" "$NEW_FILE"; then
echo "FAIL: conclusion contains a forbidden pattern"
VIOLATIONS=$((VIOLATIONS+1))
fi
if [ "$VIOLATIONS" -gt 0 ]; then
echo "Stopped: $VIOLATIONS violation(s). Fix and re-check before continuing."
exit 1
fi
echo "Guard passed."A failed run looks like this:
FAIL: title contains a forbidden pattern
FAIL: opening contains "Let's dive"
Stopped: 2 violation(s). Fix and re-check before continuing.
The important detail is exit 1. Claude Code reads the non-zero exit code as a hard stop, which means the Skill cannot move on to the push step until the article is rewritten and the same check passes. The model sees this and tries again.
Wiring up the self-correction loop
A bare exit 1 would just halt the run. To make the model actually retry, the SKILL.md should describe what happens when the guard fails. The block I use looks roughly like this:
### Recovery on guard failure
If the grep guard returns exit 1:
1. Read the violation messages and locate the offending lines in the MDX.
2. Either patch the lines with sed, or regenerate the affected section.
3. Re-run the same grep guard.
4. If the guard fails three times in a row, stop and log Status: FAILED.The retry cap matters. Without it, a stubborn pattern could trigger an unbounded loop. In practice, almost every violation clears on the first rewrite, and the second retry handles most of the remaining cases. The third retry is mostly insurance for genuinely hard topics.
What actually changed after a week
A few things I noticed after running with these guards for about a week:
- "Complete guide" and "definitive guide" disappeared from titles entirely, because grep can pin them down literally.
- Opening lines stopped starting with the most common templated phrases — that was the single biggest visible improvement.
- Some templated phrasings still surfaced in different wordings, because grep can only match exact strings. The model is creative enough to invent close variants.
That last point is worth sitting with. grep is a blunt instrument. It will not catch every paraphrase. But killing the most common 90 percent of obvious template phrases is enough to noticeably lift the felt quality of the site as a whole. The remaining 10 percent — the close paraphrases — usually need a different approach, like an LLM-as-judge pass during generation or post-publish content audits.
A second observation: do not over-constrain. If the guard list grows too long, generations start failing the cap of three retries and the FAILED log entries pile up. I started with five rules and add new ones only when the same pattern keeps appearing across multiple articles. The point of the guard is to compress the most repetitive failures, not to enforce a perfect style guide.
A note on the habit behind this
I picked up programming on my own back in 1997, when I was sixteen, by spending nights on the early Japanese internet. What stuck from that era was the sense that, when you cannot see who will read your code or your output next, you build the checks into the system rather than relying on yourself to remember. Static analysis, lint, CI — all of it comes from the same place.
Adding a grep guard to a SKILL.md is not different in spirit. It is a small note left for the future version of the Skill (and for the future version of yourself), saying: here is something I have already learned to be careful about, please do not let it slip through again. That is the part I find quietly satisfying.
There is also something almost craft-like about it. Both of my grandfathers were temple carpenters, and one phrase I grew up hearing was that if you build something carefully now, you leave the next person — including your future self — with one less thing to fight. A grep guard inside a SKILL.md feels like the same instinct, just translated into bash.
What to try next
Pick one phrase that has been bothering you in your own AI-generated outputs. Today, add a single grep -qiE ... && exit 1 line to your SKILL.md. Just one. You do not need a perfect blocklist on day one — a single check, run sixteen times a day, will already show up in next week's articles.
Thanks for reading this far. I hope a small piece of this is useful in your own setup.