<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ShipWithAI</title>
    <description>The latest articles on DEV Community by ShipWithAI (@shipwithaiio).</description>
    <link>https://dev.to/shipwithaiio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3878878%2Fd66b5c8e-e12a-4e3c-bf3b-b04ed48b4def.png</url>
      <title>DEV Community: ShipWithAI</title>
      <link>https://dev.to/shipwithaiio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shipwithaiio"/>
    <language>en</language>
    <item>
      <title>Git Worktrees for Agents: Parallel Loops Without Chaos</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Thu, 27 Aug 2026 01:30:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/git-worktrees-for-agents-parallel-loops-without-chaos-4cge</link>
      <guid>https://dev.to/shipwithaiio/git-worktrees-for-agents-parallel-loops-without-chaos-4cge</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — The moment a loop runs more than one agent, files collide. &lt;strong&gt;Worktrees&lt;/strong&gt; remove the mechanical collision (each agent gets its own working directory); the &lt;strong&gt;maker/checker split&lt;/strong&gt; removes the quality collision (no agent merges its own work). Use a three-role fleet: explorer, implementer, verifier. And the ceiling on fleet size isn't the tool — it's your review bandwidth.&lt;/p&gt;

&lt;p&gt;Part 5 of the &lt;strong&gt;Loop Engineering&lt;/strong&gt; series on ShipWithAI. &lt;a href="https://shipwithai.io/blog/loop-parallel-worktrees-subagents/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-5-intro" rel="noopener noreferrer"&gt;Read the full article →&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two agents, one directory, two different disasters
&lt;/h2&gt;

&lt;p&gt;The failure isn't one thing. It's two, and they need different fixes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Mechanical collision&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Quality collision&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Corrupted tree, broken merge, lost edits&lt;/td&gt;
&lt;td&gt;Plausible-but-wrong change lands unreviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One working directory, racing writes&lt;/td&gt;
&lt;td&gt;Author is also the only reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fixed by&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Worktree per lane&lt;/td&gt;
&lt;td&gt;Maker/checker split&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That second fix isn't house style, by the way — it's the &lt;strong&gt;evaluator-optimizer pattern&lt;/strong&gt; from Anthropic's &lt;em&gt;Building Effective AI Agents&lt;/em&gt;, where a generator and a separate evaluator pass work back and forth until the evaluator is satisfied.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Two agents in one directory is two engineers sharing one keyboard. The problem was never the agents, it was the missing isolation and the missing review.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Worktree vs branch — the distinction people get wrong
&lt;/h2&gt;

&lt;p&gt;Straight from the FAQ, because this trips up a lot of people:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A branch is a pointer in history; a worktree is a separate checked-out directory for a branch. Two agents can be on two branches but still fight over one working directory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Branches alone do not save you. Worktrees give each agent its own files on disk, which is what actually prevents the racing-write collision.&lt;/p&gt;

&lt;p&gt;And in a loop, you don't create worktrees — you &lt;em&gt;declare&lt;/em&gt; them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# fleet config: each implementer lane runs isolated&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;implementer&lt;/span&gt;
  &lt;span class="na"&gt;isolation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worktree&lt;/span&gt;     &lt;span class="c1"&gt;# orchestrator provisions a private worktree + branch per lane&lt;/span&gt;
  &lt;span class="na"&gt;on_exit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto-cleanup&lt;/span&gt;   &lt;span class="c1"&gt;# finished or failed, the dead lane's worktree is removed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In an interactive session you create a worktree; in a loop you declare one and let the orchestrator provision and reap it. The difference is &lt;strong&gt;who cleans up when no one is watching&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note this is opt-in. Subagents don't get isolation automatically — without &lt;code&gt;isolation: worktree&lt;/code&gt;, every lane shares one directory, which is exactly the collision you're trying to avoid.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three-role fleet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explorer&lt;/strong&gt; — finds work, emits a list of independent findings. It does not fix anything; it produces the work list the fleet fans out over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementer (maker)&lt;/strong&gt; — takes one finding, produces a diff in its own worktree. One implementer per lane, one finding per implementer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier (checker)&lt;/strong&gt; — reviews that diff against a verifiable stop condition, returns accept or reject.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Parallelism is for independent findings; the pipeline within a finding is always serial. An implementer that grades its own diff is not a fleet, it is &lt;strong&gt;one agent wearing two hats&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  When to parallelize, when to serialize
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Findings touch different files, no shared state&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Parallelize&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stage B needs stage A's output&lt;/td&gt;
&lt;td&gt;Serialize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two findings edit the same file&lt;/td&gt;
&lt;td&gt;Serialize (one lane)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Later finding depends on an earlier merge&lt;/td&gt;
&lt;td&gt;Serialize&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the asymmetry: you fan out across findings, never inside one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hands-on run
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the caveat first.&lt;/strong&gt; The author is explicit: this is a &lt;em&gt;mechanism proof&lt;/em&gt;, not a Haiku-graded autonomous fleet. The maker step is a deterministic edit rather than an LLM agent turn, and nothing was instrumented, so there are no token figures to report (run dated 2026-06-15). What it proves is that the orchestration plumbing — isolate, make, check, merge, reap — works as described.&lt;/p&gt;

&lt;p&gt;Worth skimming Part 3 and Part 4 first, and knowing what a worktree is — the article deliberately doesn't re-teach the git mechanic.&lt;/p&gt;

&lt;p&gt;Two independent bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finding A&lt;/strong&gt; — &lt;code&gt;mathutil.add(a, b)&lt;/code&gt; returns &lt;code&gt;a - b&lt;/code&gt;. Fails &lt;code&gt;test_mathutil.py::test_add&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finding B&lt;/strong&gt; — &lt;code&gt;strutil.shout(s)&lt;/code&gt; returns &lt;code&gt;s.lower()&lt;/code&gt;. Fails &lt;code&gt;test_strutil.py::test_shout&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Isolate:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree add ../wt-finding-a &lt;span class="nt"&gt;-b&lt;/span&gt; fix/finding-a
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree add ../wt-finding-b &lt;span class="nt"&gt;-b&lt;/span&gt; fix/finding-b
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree list
&lt;span class="go"&gt;/home/you/p5-fleet      25ee424 [master]
/home/you/wt-finding-a  25ee424 [fix/finding-a]
/home/you/wt-finding-b  25ee424 [fix/finding-b]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make&lt;/strong&gt; — each lane edits only its own file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# in wt-finding-a
 def add(a, b):
&lt;span class="gd"&gt;-    # BUG (Finding A): returns difference instead of sum
-    return a - b
&lt;/span&gt;&lt;span class="gi"&gt;+    # FIX (Finding A): correct sum
+    return a + b
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lane B gets the same treatment on &lt;code&gt;strutil.py&lt;/code&gt;. And the isolation is real, not asserted — from inside lane A, lane B's file is still broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;HEAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;strutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;      &lt;span class="c1"&gt;# in wt-finding-a
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# BUG (Finding B): lowercases instead of uppercasing
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check&lt;/strong&gt; — each verifier runs only its lane's test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest &lt;span class="nt"&gt;-q&lt;/span&gt; test_mathutil.py    &lt;span class="c"&gt;# in wt-finding-a&lt;/span&gt;
&lt;span class="gp"&gt;1 passed in 0.00s               #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest &lt;span class="nt"&gt;-q&lt;/span&gt; test_strutil.py     &lt;span class="c"&gt;# in wt-finding-b&lt;/span&gt;
&lt;span class="gp"&gt;1 passed in 0.00s               #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Merge&lt;/strong&gt; — both lanes land in one state file (Part 4's schema), findings moving &lt;code&gt;open&lt;/code&gt; → &lt;code&gt;tried&lt;/code&gt; + &lt;code&gt;passed&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## tried&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [finding-a] worktree wt-finding-a / branch fix/finding-a: maker edited mathutil.py (a-b to a+b)
&lt;span class="p"&gt;-&lt;/span&gt; [finding-b] worktree wt-finding-b / branch fix/finding-b: maker edited strutil.py (lower to upper)

&lt;span class="gu"&gt;## passed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [finding-a] test_mathutil.py::test_add green in wt-finding-a (checker pytest exit 0)
&lt;span class="p"&gt;-&lt;/span&gt; [finding-b] test_strutil.py::test_shout green in wt-finding-b (checker pytest exit 0)

&lt;span class="gu"&gt;## open&lt;/span&gt;

&lt;span class="gu"&gt;## blocked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The merge is the moment of truth. Two isolated lanes are only useful if their results land in one place the next run can read, and that place is the state file from Part 4.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Reap:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree remove ../wt-finding-a
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree remove ../wt-finding-b
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree list
&lt;span class="go"&gt;/home/you/p5-fleet  25ee424 [master]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a lane fails mid-run and you have &lt;code&gt;on_exit: auto-cleanup&lt;/code&gt; set, its worktree is removed so the loop doesn't accumulate orphan directories across iterations — and the finding stays &lt;code&gt;open&lt;/code&gt; (or moves to &lt;code&gt;blocked&lt;/code&gt;), so the next run retries without re-deriving what already happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The orchestration tax
&lt;/h2&gt;

&lt;p&gt;Every lane you add is a trade, not a free win:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Per lane added&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token cost&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Up&lt;/strong&gt; (≈ N× per-agent cost + orchestration overhead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall-clock to results&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Down&lt;/strong&gt; (lanes run concurrently)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your review load&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Up&lt;/strong&gt; (every green diff still needs a human merge)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third row is the one that bites.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Worktrees and checkers let you run more agents. Your review bandwidth decides how many you &lt;em&gt;should&lt;/em&gt;. The tool's ceiling and your ceiling are not the same number, &lt;strong&gt;and yours is lower&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;if you can honestly review N PRs a day, run at most N lanes.&lt;/strong&gt; The author flags this as his own framing — a heuristic, not a measured constant. Calibrate it to your team.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prior art
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/smtg-ai/claude-squad" rel="noopener noreferrer"&gt;claude-squad&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/Dicklesworthstone/claude_code_agent_farm" rel="noopener noreferrer"&gt;claude_code_agent_farm&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/AnandChowdhary/continuous-claude" rel="noopener noreferrer"&gt;continuous-claude&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus Anthropic's &lt;em&gt;Building Effective AI Agents&lt;/em&gt; for the evaluator-optimizer pattern that the maker/checker split descends from.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it this week
&lt;/h2&gt;

&lt;p&gt;Take two genuinely independent bugs. Give each its own worktree and branch. Have something other than the fixer run the test. Merge both into one state file, then reap the worktrees.&lt;/p&gt;

&lt;p&gt;If you can't name why the two findings are independent, you have one lane, not two.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This is a condensed summary.&lt;/strong&gt; The full article has the complete fleet walkthrough, both diffs, and the FAQ on failure handling and fleet sizing:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://shipwithai.io/blog/loop-parallel-worktrees-subagents/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-5-cta" rel="noopener noreferrer"&gt;Git Worktrees for Agents: Parallel Loops Without Chaos&lt;/a&gt;&lt;/strong&gt; — Part 5, ShipWithAI&lt;/p&gt;

&lt;p&gt;Earlier: &lt;a href="https://shipwithai.io/blog/loop-engineering-from-harness-to-loops/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-5-prev1" rel="noopener noreferrer"&gt;Part 1 — Why You Should Stop Prompting&lt;/a&gt; · &lt;a href="https://shipwithai.io/blog/loop-anatomy-five-building-blocks/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-5-prev2" rel="noopener noreferrer"&gt;Part 2 — Anatomy of a Loop&lt;/a&gt; · &lt;a href="https://shipwithai.io/blog/loop-stop-conditions-verification/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-5-prev3" rel="noopener noreferrer"&gt;Part 3 — Stop Conditions&lt;/a&gt; · &lt;a href="https://shipwithai.io/blog/loop-memory-state-files/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-5-prev4" rel="noopener noreferrer"&gt;Part 4 — State File Pattern&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next: giving the fleet a heartbeat — scheduled discovery, and runs that archive themselves.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>programming</category>
      <category>claude</category>
    </item>
    <item>
      <title>Give Your Agent Loop a Heartbeat: Scheduled Automation Without the Runaway Bill</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Tue, 25 Aug 2026 01:30:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/give-your-agent-loop-a-heartbeat-scheduled-automation-without-the-runaway-bill-404i</link>
      <guid>https://dev.to/shipwithaiio/give-your-agent-loop-a-heartbeat-scheduled-automation-without-the-runaway-bill-404i</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A loop earns its name when it runs on a cadence you didn't trigger. Three heartbeats: in-session &lt;code&gt;/loop&lt;/code&gt; (you're at the keyboard), cloud Routines (cron, laptop closed), and CI (GitHub Actions). Pick by where the work lives and who needs to be awake. Then: route findings to an inbox a human reads, and make the empty run nearly free — because on a daily cadence, &lt;strong&gt;the empty run is the common case&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fourth post in my **Loop Engineering&lt;/em&gt;* crosspost series here — it summarizes &lt;strong&gt;Part 6&lt;/strong&gt; of the original series on ShipWithAI. (I'm jumping ahead: Parts 4 and 5 over there cover state files and worktrees, and this one builds on both. Links at the bottom.)*&lt;/p&gt;

&lt;p&gt;&lt;a href="https://shipwithai.io/blog/loop-scheduled-automations-heartbeat/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-intro" rel="noopener noreferrer"&gt;Read the full article →&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Three heartbeats
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Heartbeat&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Laptop state&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In-session &lt;code&gt;/loop&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A prompt re-run on an interval&lt;/td&gt;
&lt;td&gt;Open, you're at the keyboard&lt;/td&gt;
&lt;td&gt;An attended sweep you babysit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Routines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5-field cron on Anthropic infra (daily cap on runs started)&lt;/td&gt;
&lt;td&gt;Closed&lt;/td&gt;
&lt;td&gt;The true overnight or morning cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI (GitHub Actions)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on: schedule&lt;/code&gt; or push in a workflow&lt;/td&gt;
&lt;td&gt;Off (runs on a runner)&lt;/td&gt;
&lt;td&gt;When the work is the repo and the team lives in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;The schedule is the easy half. A cron job that runs a fixed script is just cron; a cron job that runs a &lt;strong&gt;decision-maker&lt;/strong&gt; is a loop. Pick the heartbeat by who needs to be awake: you, no one, or the CI runner.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The article opens with Matt Van Horn's line: &lt;em&gt;a loop is cron plus a decision-maker in the body.&lt;/em&gt; The schedule is the trivial half. Each run reads state, decides what to do, and updates that state, so the next run continues where this one stopped instead of repeating a static command.&lt;/p&gt;

&lt;p&gt;One planning detail that's easy to miss: cloud Routines carry a daily cap on how many runs can start. If your cost model assumes you can just dial frequency up, that cap is the ceiling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hook or schedule?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger type&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Hook or schedule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lifecycle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Something happens in a session&lt;/td&gt;
&lt;td&gt;A tool runs; a stop is attempted&lt;/td&gt;
&lt;td&gt;Hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The clock ticks, regardless&lt;/td&gt;
&lt;td&gt;06:00 weekdays; every push&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;A hook is a reflex; a schedule is a heartbeat. Reflexes fire when something happens. A heartbeat fires whether or not anything did, which is exactly why a scheduled loop needs the triage inbox next.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many loops use both: a schedule wakes the loop, hooks govern each turn inside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The triage inbox — the part that keeps this safe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scheduled run
  |
  |-- findings?  --&amp;gt;  inbox (state file / issue / PR)  --&amp;gt;  human decides  --&amp;gt; merge
  |                        (the loop never merges its own findings)
  |
  '-- nothing?   --&amp;gt;  archive "nothing today"  --&amp;gt;  exit cheap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;An unattended loop is allowed to find work and allowed to do the boring parts. It is &lt;strong&gt;never&lt;/strong&gt; allowed to decide what ships. The inbox is where the loop stops and you start.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The article calls auto-merge "the cardinal sin of unattended automation," while flagging that the inbox pattern is its own synthesis rather than a cited standard — it just considers it the single most important discipline in the post. Discovery is automated; the decision stays human.&lt;/p&gt;




&lt;h2&gt;
  
  
  The morning triage loop
&lt;/h2&gt;

&lt;p&gt;Two failing tests in yesterday's CI — a real pytest suite, two genuine independent failures (&lt;code&gt;slugify&lt;/code&gt; leaving a trailing hyphen, &lt;code&gt;load_port&lt;/code&gt; returning a string instead of an int) — plus three open issues, two of which map to those failures.&lt;/p&gt;

&lt;p&gt;The schedule, as a GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;morning-triage&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1-5"&lt;/span&gt;   &lt;span class="c1"&gt;# 06:00 on weekdays, the heartbeat&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;triage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run morning triage (discover, write inbox, open lanes)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 triage/triage.py --budget-runs 1 --max-lanes &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the same beat as a plain crontab line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt; * * &lt;span class="m"&gt;1&lt;/span&gt;-&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt; /&lt;span class="n"&gt;home&lt;/span&gt;/&lt;span class="n"&gt;you&lt;/span&gt;/&lt;span class="n"&gt;p6&lt;/span&gt;-&lt;span class="n"&gt;triage&lt;/span&gt; &amp;amp;&amp;amp; /&lt;span class="n"&gt;usr&lt;/span&gt;/&lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;python3&lt;/span&gt; &lt;span class="n"&gt;triage&lt;/span&gt;/&lt;span class="n"&gt;triage&lt;/span&gt;.&lt;span class="n"&gt;py&lt;/span&gt; --&lt;span class="n"&gt;max&lt;/span&gt;-&lt;span class="n"&gt;lanes&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; --&lt;span class="n"&gt;budget&lt;/span&gt;-&lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &amp;gt;&amp;gt; $&lt;span class="n"&gt;HOME&lt;/span&gt;/.&lt;span class="n"&gt;morning&lt;/span&gt;-&lt;span class="n"&gt;triage&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&amp;amp;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the honest part, and read it before the output: &lt;strong&gt;nothing scheduled actually fired.&lt;/strong&gt; The sandbox has no cron daemon, so the cron line — the real artifact you'd install — was never triggered by cron. Instead the exact triage step the cron line invokes was run directly, which is what the heartbeat executes either way. Same for the Actions workflow: not a cloud-executed run.&lt;/p&gt;

&lt;p&gt;So, the triage step invoked by hand — note what it refuses to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 triage/triage.py &lt;span class="nt"&gt;--max-lanes&lt;/span&gt; 2 &lt;span class="nt"&gt;--budget-runs&lt;/span&gt; 1
&lt;span class="gp"&gt;TRIAGE: 3 findings;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;opened 2 lanes&lt;span class="p"&gt;;&lt;/span&gt; 1 left &lt;span class="k"&gt;in &lt;/span&gt;inbox&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;passed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;(&lt;/span&gt;never auto-merge&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git worktree list
&lt;span class="go"&gt;/home/you/p6-triage        242b6dc [master]
/home/you/wt-finding-14    242b6dc [triage/finding-14]
/home/you/wt-finding-15    242b6dc [triage/finding-15]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ranking logic: CI-red bugs above issue-only, bugs above features, top two get lanes. Finding #19 (a feature request) stays in the inbox with no lane.&lt;/p&gt;

&lt;p&gt;And the resulting state file, in Part 4's &lt;code&gt;tried/passed/open/blocked&lt;/code&gt; schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Loop State: morning triage (Part 4 schema)&lt;/span&gt;

&lt;span class="gu"&gt;## tried&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; read CI: 2 failing test(s); read issues: 3 open
&lt;span class="p"&gt;-&lt;/span&gt; opened 2 worktree lane(s) for the top 2 finding(s)

&lt;span class="gu"&gt;## passed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; (none; triage never merges; a human decides what ships)

&lt;span class="gu"&gt;## open&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [#14 CI-red: tests/test_slugify.py::test_trailing_hyphen] lane opened: /home/you/wt-finding-14 on triage/finding-14 (awaiting human)
&lt;span class="p"&gt;-&lt;/span&gt; [#15 CI-red: tests/test_config.py::test_port_is_int] lane opened: /home/you/wt-finding-15 on triage/finding-15 (awaiting human)
&lt;span class="p"&gt;-&lt;/span&gt; [#19 [feature] add a --version flag to the CLI] in inbox, no lane (below top-2)

&lt;span class="gu"&gt;## blocked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;## passed&lt;/code&gt; section being empty is not a bug. It's the design.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The morning loop is the whole series in one run: a schedule wakes it (Part 6), a stop condition bounds it (Part 3), the findings land in the state file (Part 4), and the top two get worktree lanes (Part 5). The cadence is the only new part.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To restate the caveat plainly: this is a &lt;strong&gt;mechanism proof&lt;/strong&gt;. Neither a live cloud-Routine nor a cloud-executed Actions run is cleanly capturable headlessly — the same limit documented in the Part 3, 4, and 5 traces. And the decision-maker here is deterministic ranking logic, not an LLM agent turn, so nothing was instrumented and there's no token or cost figure to report.&lt;/p&gt;




&lt;h2&gt;
  
  
  The empty run is your real cost
&lt;/h2&gt;

&lt;p&gt;Here's the clean-morning case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pytest &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="go"&gt;..                                    [100%]
2 passed in 0.00s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 triage/triage.py &lt;span class="nt"&gt;--max-lanes&lt;/span&gt; 2 &lt;span class="nt"&gt;--budget-runs&lt;/span&gt; 1
&lt;span class="gp"&gt;EMPTY RUN: 0 findings -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;archived &lt;span class="s1"&gt;'nothing today'&lt;/span&gt;, &lt;span class="nb"&gt;exit &lt;/span&gt;cheap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;On a daily cadence, the empty run is the common case, so the empty run &lt;strong&gt;is&lt;/strong&gt; your real cost. Cap every run and make "nothing today" nearly free, or the heartbeat quietly drains the budget while you sleep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four kill switches, bounding every run rather than just the whole job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kill switch&lt;/th&gt;
&lt;th&gt;What it bounds&lt;/th&gt;
&lt;th&gt;Failure it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-runs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Iterations per invocation&lt;/td&gt;
&lt;td&gt;An infinite re-run loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-cost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dollars per run&lt;/td&gt;
&lt;td&gt;A runaway bill overnight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wall-clock per run&lt;/td&gt;
&lt;td&gt;A hung run that never exits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--stall-threshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Iterations with no progress&lt;/td&gt;
&lt;td&gt;Spinning without converging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Prior art worth a look
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/thebasedcapital/nightcrawler" rel="noopener noreferrer"&gt;nightcrawler&lt;/a&gt;&lt;/strong&gt; — autonomous overnight research loop, episodic execution with a per-run episode budget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/githubnext/agentics" rel="noopener noreferrer"&gt;agentics&lt;/a&gt;&lt;/strong&gt; — frames "Continuous AI" as GitHub Actions workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Jedward23/Tmux-Orchestrator" rel="noopener noreferrer"&gt;Tmux-Orchestrator&lt;/a&gt;&lt;/strong&gt; — self-scheduling agents; dormant since mid-2025, but instructive&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it this week
&lt;/h2&gt;

&lt;p&gt;Pick your beat by who needs to be awake. Write the discovery step. Then — before you schedule anything — write the inbox, and confirm your loop has no path to merge.&lt;/p&gt;

&lt;p&gt;Then time the empty run and multiply by your cadence — weekdays at 06:00 is ~261 runs a year. A daily cadence multiplies cost by frequency, and the empty run is what you're multiplying.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This is a condensed summary.&lt;/strong&gt; The full article has the step-by-step build, worktree cleanup, and the FAQ on cron vs Routines vs CI:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://shipwithai.io/blog/loop-scheduled-automations-heartbeat/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-cta" rel="noopener noreferrer"&gt;Scheduled Agent Automation: Give Your Loop a Heartbeat&lt;/a&gt;&lt;/strong&gt; — Part 6, ShipWithAI&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the two I skipped first if you want the full build&lt;/strong&gt; — this post leans on both: &lt;a href="https://shipwithai.io/blog/loop-memory-state-files/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-skipped4" rel="noopener noreferrer"&gt;Part 4 — State File Pattern&lt;/a&gt; (the &lt;code&gt;tried/passed/open/blocked&lt;/code&gt; schema) and &lt;a href="https://shipwithai.io/blog/loop-parallel-worktrees-subagents/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-skipped5" rel="noopener noreferrer"&gt;Part 5 — Git Worktrees for Agents&lt;/a&gt; (the lanes).&lt;/p&gt;

&lt;p&gt;Earlier in the series: &lt;a href="https://shipwithai.io/blog/loop-engineering-from-harness-to-loops/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-prev1" rel="noopener noreferrer"&gt;Part 1 — Why You Should Stop Prompting&lt;/a&gt; · &lt;a href="https://shipwithai.io/blog/loop-anatomy-five-building-blocks/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-prev2" rel="noopener noreferrer"&gt;Part 2 — Anatomy of a Loop&lt;/a&gt; · &lt;a href="https://shipwithai.io/blog/loop-stop-conditions-verification/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-4-prev3" rel="noopener noreferrer"&gt;Part 3 — Stop Conditions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next up: the journey log of retrofitting all of this onto a real, messy first loop — the start of the practice phase.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Your Agent Loop Is Lying About Being Done: Writing Verifiable Stop Conditions</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Sun, 23 Aug 2026 01:30:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/your-agent-loop-is-lying-about-being-done-writing-verifiable-stop-conditions-270p</link>
      <guid>https://dev.to/shipwithaiio/your-agent-loop-is-lying-about-being-done-writing-verifiable-stop-conditions-270p</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A loop is only as trustworthy as its stop condition. "Done" has to be checkable by a &lt;em&gt;different&lt;/em&gt; model — tests pass, lint clean, schema validates — never the worker's own claim. Below: why self-grading fails, the four rungs of verification, a checker-agent template you can copy, and the three caps that stop a loop running forever.&lt;/p&gt;

&lt;p&gt;Part 3 of the &lt;strong&gt;Loop Engineering&lt;/strong&gt; series on ShipWithAI. &lt;a href="https://shipwithai.io/blog/loop-stop-conditions-verification/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-3-intro" rel="noopener noreferrer"&gt;Read the full article →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Part 2 gave you the parts list. Part 1 promised this installment would be entirely about designing stop conditions, "because they are the hardest part to get right." Here it is.&lt;/p&gt;

&lt;p&gt;The thesis, in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A loop that grades its own work will tell you it is done before it is.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why self-grading fails
&lt;/h2&gt;

&lt;p&gt;A stop condition is a &lt;em&gt;claim about reality&lt;/em&gt;. If the agent that wrote the code is the only thing asserting the claim, you don't have a verification — you have a &lt;strong&gt;self-report&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The author's framing: a maker grading its own output is &lt;strong&gt;structurally optimistic&lt;/strong&gt;, so it declares victory early. And this has a name — Anthropic's own writeup on building effective agents lists the failure mode explicitly: &lt;em&gt;premature victory and fake-done features.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The four rungs
&lt;/h2&gt;

&lt;p&gt;The rungs are an escalation of &lt;em&gt;who checks&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it adds over the rung below&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bare &lt;code&gt;while ! npm test; do claude -p ...; done&lt;/code&gt; + iteration cap&lt;/td&gt;
&lt;td&gt;A machine-checkable exit code as the gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stop hook (&lt;code&gt;exit 2&lt;/code&gt; or &lt;code&gt;{decision:"block"}&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Blocks completion until the check passes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/goal&lt;/code&gt; — a separate model grades each turn&lt;/td&gt;
&lt;td&gt;A grader that is &lt;strong&gt;not&lt;/strong&gt; the maker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scheduled goal runs (Routines)&lt;/td&gt;
&lt;td&gt;The condition is re-checked unattended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Rung 1 checks with an exit code, rung 3 checks with a second model, and the gap between them is exactly the maker's optimism.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Worth knowing the mechanics of rung 2 vs 3: a Stop hook blocks completion with &lt;code&gt;exit 2&lt;/code&gt; until a check passes, and &lt;strong&gt;Claude Code auto-overrides after 8 consecutive blocks&lt;/strong&gt;. &lt;code&gt;/goal&lt;/code&gt; (v2.1.139+) instead has a separate model — Haiku — grade the stop condition each turn.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rewriting a wish into a condition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Run until the feature is done" is not a stop condition, it is a wish. A stop condition is a command with an exit code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bad condition&lt;/th&gt;
&lt;th&gt;Why it fails&lt;/th&gt;
&lt;th&gt;Verifiable rewrite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"the feature is done"&lt;/td&gt;
&lt;td&gt;Not falsifiable, no check&lt;/td&gt;
&lt;td&gt;"all tests in &lt;code&gt;test/auth&lt;/code&gt; pass AND lint clean"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"code looks good"&lt;/td&gt;
&lt;td&gt;Not verifiable by a machine&lt;/td&gt;
&lt;td&gt;"&lt;code&gt;passes: true&lt;/code&gt; for every story in &lt;code&gt;prd.json&lt;/code&gt;"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"the bug is fixed"&lt;/td&gt;
&lt;td&gt;No boundary&lt;/td&gt;
&lt;td&gt;"the new regression test exits 0 and no other test breaks"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three-attribute test for any condition you write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verifiable&lt;/strong&gt; — something can confirm it (a command, a count, a second model)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falsifiable&lt;/strong&gt; — it can fail, with a clear failing signal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap&lt;/strong&gt; — you can evaluate it every single turn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the one people underrate — "cheap" here means literally &lt;em&gt;you can run it every turn&lt;/em&gt;, not that it's nice to have.&lt;/p&gt;




&lt;h2&gt;
  
  
  The checker agent
&lt;/h2&gt;

&lt;p&gt;Copy this. The whole design is in what it's forbidden from doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stop-condition-checker&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Grades whether the loop's stop condition holds. Not the maker.&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;haiku&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="s"&gt;You verify, you do not fix. Run the project's check command and report only&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PASS if `bash tests/run.sh` exits 0 AND `bash scripts/lint.sh` exits &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;FAIL otherwise, with the first failing line&lt;/span&gt;

&lt;span class="s"&gt;Never edit code. Never report PASS on the maker's say-so; run the command.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The checker's value comes entirely from not being the maker. Different instructions, a cheaper model, and a clean context each turn are what make its PASS mean something.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The article's actual prescription: a checker with different instructions, &lt;em&gt;optionally&lt;/em&gt; a different model, and ideally a clean context each turn. The different model is an option, not the requirement — the requirement is not being the maker. Splitting maker from checker is called "the single highest-leverage move in loop design."&lt;/p&gt;

&lt;p&gt;One real trade-off worth knowing: the &lt;strong&gt;official ralph-wiggum plugin&lt;/strong&gt; runs the checker in the same session as the maker, and the community has flagged that this deviates from fresh-context Ralph, where each lap starts clean. The article doesn't pick a side — &lt;em&gt;same-session is cheaper; fresh-context is harder to fool. Pick deliberately.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The hands-on run
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the disclaimer first&lt;/strong&gt;, because it's the most useful part. The run did &lt;em&gt;not&lt;/em&gt; use rung 3. &lt;code&gt;/goal&lt;/code&gt; was the natural fit, but it's an interactive in-session grader and the author wanted a captured, reproducible run — so he used the &lt;strong&gt;rung-1 bare-loop equivalent&lt;/strong&gt;: a fresh-context &lt;code&gt;claude -p&lt;/code&gt; as the maker each turn, and the bash test exit code as the checker. An honest maker/checker split, graded by a machine-checkable artifact rather than by Haiku. He also notes token and dollar cost weren't instrumented, so he won't quote a number he doesn't have.&lt;/p&gt;

&lt;p&gt;The bug, in ShipWithAI's content-agent repo: &lt;code&gt;scripts/check-draft-seo.sh&lt;/code&gt; compares every keyword word against only the title's first three words — so &lt;strong&gt;any keyword longer than three words&lt;/strong&gt; gets flagged, even when it leads the title verbatim.&lt;/p&gt;

&lt;p&gt;The failing test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠ keyword_placement: keyword "how to build an agent loop": not in title first 3 words
FAIL: multi-word keyword that leads the title was wrongly flagged.
exit=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stop condition — note that it guards against collateral damage, not just the target bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash tests/test-check-draft-seo-multiword-keyword.sh exits 0
  AND bash scripts/test-rubric.sh still exits 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix, one line of real logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;all_title_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z...]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;kw_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z...]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kw_lower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;title_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;all_title_words&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kw_words&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: one fresh-context maker turn. Pass on turn 1, cap of 8 never reached.&lt;/strong&gt; He also ran a control draft to prove the fix isn't just a check that now always passes — worth copying as a habit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The loop stopped because a command exited 0, not because the agent felt finished. That is the only kind of "done" you can leave a loop alone with.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Cap it three ways
&lt;/h2&gt;

&lt;p&gt;The stop condition says when to stop &lt;strong&gt;on success&lt;/strong&gt;. The cap says when to stop &lt;strong&gt;anyway&lt;/strong&gt;. A loop you trust has both.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Max iteration count&lt;/strong&gt; — &lt;code&gt;claude -p --max-turns N&lt;/code&gt;, plus the Stop hook's 8-block auto-override&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-progress detection&lt;/strong&gt; — continuous-claude's &lt;code&gt;--stall-threshold&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget ceiling&lt;/strong&gt; — &lt;code&gt;--max-cost&lt;/code&gt; and &lt;code&gt;--max-duration&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For lived precedent: ShipWithAI's own content-agent review loop caps at &lt;strong&gt;3&lt;/strong&gt;. The hands-on run above capped at 8. Pick per job, not by rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you try this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A working harness and a minimal loop you can already run&lt;/li&gt;
&lt;li&gt;Claude Code &lt;strong&gt;v2.1.139+&lt;/strong&gt; for &lt;code&gt;/goal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A repo with a test command that exits non-zero on failure&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it this week
&lt;/h2&gt;

&lt;p&gt;Take the loop you built after Part 2 and ask one question: &lt;em&gt;who asserts that it's done?&lt;/em&gt; If the answer is "the same agent that did the work," you haven't reached rung 1 yet.&lt;/p&gt;

&lt;p&gt;Write the condition as a shell command. Add a second agent whose only job is to run it. Then pick your three caps.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This is a condensed summary.&lt;/strong&gt; The full article walks each rung with working config, the complete hands-on trace, and the FAQ on &lt;code&gt;/goal&lt;/code&gt; vs Stop hooks:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://shipwithai.io/blog/loop-stop-conditions-verification/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-3-cta" rel="noopener noreferrer"&gt;Stop Conditions: Making "Done" Mean Something&lt;/a&gt;&lt;/strong&gt; — Part 3, ShipWithAI&lt;/p&gt;

&lt;p&gt;Earlier: &lt;a href="https://shipwithai.io/blog/loop-engineering-from-harness-to-loops/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-3-prev1" rel="noopener noreferrer"&gt;Part 1 — Why You Should Stop Prompting&lt;/a&gt; · &lt;a href="https://shipwithai.io/blog/loop-anatomy-five-building-blocks/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-3-prev2" rel="noopener noreferrer"&gt;Part 2 — Anatomy of a Loop&lt;/a&gt;. Part 4 — &lt;em&gt;Memory Outside the Context Window&lt;/em&gt; — ships next.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
    <item>
      <title>Anatomy of an Agent Loop: 5 Building Blocks and One Memory Spine</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:30:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/anatomy-of-an-agent-loop-5-building-blocks-and-one-memory-spine-2i98</link>
      <guid>https://dev.to/shipwithaiio/anatomy-of-an-agent-loop-5-building-blocks-and-one-memory-spine-2i98</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — An agent loop is five blocks doing work plus a &lt;strong&gt;memory spine&lt;/strong&gt; that makes the work cumulative. Drop the spine and you have automation that forgets every morning. Below: the map, the primitive each block maps to in Claude Code, and a real lint-sweeper loop that cleared 3 structural findings in 2 iterations / 124 seconds.&lt;/p&gt;

&lt;p&gt;Part 2 of the &lt;strong&gt;Loop Engineering&lt;/strong&gt; series on ShipWithAI. &lt;a href="https://shipwithai.io/blog/loop-anatomy-five-building-blocks/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-2-intro" rel="noopener noreferrer"&gt;Read the full article →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Part 1 argued &lt;em&gt;why&lt;/em&gt; you should stop prompting turn by turn. This one is the parts list.&lt;/p&gt;

&lt;p&gt;Working definition, unchanged: &lt;strong&gt;a loop is a recursive goal&lt;/strong&gt; — you define a purpose and a verifiable stop condition, and the system iterates agents against it until the condition holds, without you prompting each turn.&lt;/p&gt;




&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Scheduled automation (heartbeat)

2. Worktrees            3. Skills
          \             /
          [ MEMORY SPINE ]   &amp;lt;- state on disk, survives runs
          /             \
5. Sub-agents       4. Plugins / connectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framing is Addy Osmani's. Each block maps to something you already have:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Block&lt;/th&gt;
&lt;th&gt;Claude Code primitive&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scheduled automation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/loop [interval]&lt;/code&gt;; Routines for cloud cron&lt;/td&gt;
&lt;td&gt;Re-running a check on a cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Worktrees&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isolation: worktree&lt;/code&gt; in agent frontmatter&lt;/td&gt;
&lt;td&gt;Parallel work without file collisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Externalizing project intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Plugins / connectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Plugins and MCP servers&lt;/td&gt;
&lt;td&gt;Reaching real tools (GitHub, CI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sub-agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Files in &lt;code&gt;.claude/agents/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The maker/checker split&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;On block 1&lt;/strong&gt; — &lt;code&gt;/loop&lt;/code&gt; re-runs a prompt on an interval &lt;em&gt;while your session is open&lt;/em&gt;. Routines run cloud-scheduled on cron with the laptop closed (there's a per-account daily cap). Different tools, same job.&lt;/p&gt;

&lt;p&gt;Nothing here is exotic. &lt;strong&gt;The skill is assembly&lt;/strong&gt;: picking which blocks a given job needs and wiring them around one state file.&lt;/p&gt;




&lt;h2&gt;
  
  
  The spine is the part people skip
&lt;/h2&gt;

&lt;p&gt;The five blocks do the work. The spine is what makes the work &lt;em&gt;cumulative&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Concretely: a small file on disk that survives between runs. Greppable, versioned, boring. &lt;code&gt;snarktank/ralph&lt;/code&gt; (14.3k stars) does it with git plus a &lt;code&gt;progress.txt&lt;/code&gt; and a &lt;code&gt;prd.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without it, iteration 4 has no idea what iterations 1 through 3 already tried. That's not a loop, that's a cron job with amnesia.&lt;/p&gt;




&lt;h2&gt;
  
  
  A real, minimal loop you can copy
&lt;/h2&gt;

&lt;p&gt;The spec uses the standard kickoff template (from the loops.elorm.xyz card format), which fits the &lt;code&gt;/goal&lt;/code&gt; contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lint-sweeper.loop&lt;/span&gt;

&lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flake8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--extend-ignore=E501&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scripts/*.py&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exits&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
&lt;span class="na"&gt;max_iterations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;between_iterations_check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flake8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--extend-ignore=E501&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scripts/*.py"&lt;/span&gt;
&lt;span class="na"&gt;exit_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exits&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt;  &lt;/span&gt;&lt;span class="s"&gt;OR&lt;/span&gt;&lt;span class="nv"&gt;  &lt;/span&gt;&lt;span class="s"&gt;iteration&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;max_iterations"&lt;/span&gt;
&lt;span class="na"&gt;one_step&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;batch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;findings&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(batch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;then&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;re-run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;check"&lt;/span&gt;
&lt;span class="na"&gt;self_pace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;as&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;soon&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;as&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit_when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;holds;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;keep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;going"&lt;/span&gt;
&lt;span class="na"&gt;state_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drafts/lint-sweeper-state.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the spine it writes to, at iteration 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;goal: flake8 --extend-ignore=E501 scripts/&lt;span class="err"&gt;*&lt;/span&gt;.py exits 0
cap: 5 iterations
iteration: 0

&lt;span class="gu"&gt;## open&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; gen-agent-map.py:19 F401 'os' imported but unused
&lt;span class="p"&gt;-&lt;/span&gt; gen-dist-metadata.py:23 E301 expected 1 blank line
&lt;span class="p"&gt;-&lt;/span&gt; gen-dist-metadata.py:27 E301 expected 1 blank line

&lt;span class="gu"&gt;## fixed&lt;/span&gt;
&lt;span class="gu"&gt;## skipped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what &lt;code&gt;goal&lt;/code&gt; is: a &lt;strong&gt;shell command and its exit code&lt;/strong&gt;, not an English sentence about quality. As the article puts it — the check is the stop signal, so it must be machine-checkable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happened when it ran
&lt;/h2&gt;

&lt;p&gt;Two honest caveats from the author before the numbers. This ran on the Python scripts in ShipWithAI's content-agent plugin repo, &lt;em&gt;because shipwithai.io has no lint command&lt;/em&gt; — a substitution he flags himself. And flake8 actually reported 41 findings; 38 were E501 (line-too-long), which is noisy and subjective, so the verifier was scoped to the 3 real structural defects.&lt;/p&gt;

&lt;p&gt;Baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flake8 &lt;span class="nt"&gt;--extend-ignore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;E501 scripts/&lt;span class="k"&gt;*&lt;/span&gt;.py
&lt;span class="go"&gt;scripts/gen-agent-map.py:19:1: F401 'os' imported but unused
scripts/gen-dist-metadata.py:23:5: E301 expected 1 blank line, found 0
scripts/gen-dist-metadata.py:27:5: E301 expected 1 blank line, found 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After iteration 1&lt;/strong&gt; — one file's worth of findings gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scripts/gen-dist-metadata.py:23:5: E301 expected 1 blank line, found 0
scripts/gen-dist-metadata.py:27:5: E301 expected 1 blank line, found 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After iteration 2:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# no output, exit 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final tally: &lt;strong&gt;3 structural findings → 0, in 2 of 5 allowed iterations, 124 seconds wall-clock.&lt;/strong&gt; Clean exit — it never touched the cap. Total damage: 2 files changed, 2 insertions, 1 deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading the trace
&lt;/h2&gt;

&lt;p&gt;This is the part worth internalizing. Each block leaves a fingerprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat&lt;/strong&gt; — fired the check 3 times (baseline + 2 iterations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier&lt;/strong&gt; — gated each iteration on flake8's exit code. Exit 1 → continue. Exit 0 → stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory spine&lt;/strong&gt; — recorded each finding moving &lt;code&gt;open&lt;/code&gt; → &lt;code&gt;fixed&lt;/code&gt;, tagged by iteration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maker&lt;/strong&gt; — produced the diff, handed it to the verifier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worktrees and plugins/connectors&lt;/strong&gt; — never used&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The article is careful here, and so should you be: this run used a &lt;strong&gt;single agent&lt;/strong&gt; with the lint exit code as the checker. The real sub-agent maker/checker split — where a separate agent grades the work — is Part 3 and Part 5 material. It's named in the map, not claimed in the trace. Four pieces exercised, two honestly skipped.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The trace is how you debug a loop. Each block leaves a fingerprint, and the verifier's exit code, not the agent's confidence, is what ends the run.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What breaks when a block is missing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Missing&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verifiable stop condition&lt;/td&gt;
&lt;td&gt;The loop declares false victory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory spine&lt;/td&gt;
&lt;td&gt;Every run starts from zero, no resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worktrees / maker-checker&lt;/td&gt;
&lt;td&gt;Parallel agents collide on the same files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A real heartbeat&lt;/td&gt;
&lt;td&gt;You have a script you ran once, not a loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of those gets its own part later in the series — the heartbeat one is Part 6.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you try this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A working harness — CLAUDE.md, at least one skill, a hook or two&lt;/li&gt;
&lt;li&gt;Claude Code &lt;strong&gt;v2.1.139+&lt;/strong&gt; for &lt;code&gt;/goal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A repo with a lint command that exits non-zero when it should&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And no, you don't need worktrees for a single-agent loop. They earn their place once more than one agent touches the repo. The lint sweeper ran fine without one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it this week
&lt;/h2&gt;

&lt;p&gt;Pick the smallest thing in your repo where "done" is already an exit code — lint, a type-check, one failing test. Write the spec (goal, cap, check command, exit condition, one-step, self-pace). Add the state file. Cap it at 5.&lt;/p&gt;

&lt;p&gt;If it exits clean before the cap, you've built a loop. If it declares victory while the check still fails, your stop condition isn't verifiable — which is exactly what Part 3 is about.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This is a condensed summary.&lt;/strong&gt; The full article walks the setup step by step, shows the full diff, and answers the &lt;code&gt;/loop&lt;/code&gt; vs Routines and do-I-need-worktrees questions:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://shipwithai.io/blog/loop-anatomy-five-building-blocks/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-2-cta" rel="noopener noreferrer"&gt;Anatomy of a Loop: Five Building Blocks and One Spine&lt;/a&gt;&lt;/strong&gt; — Part 2, ShipWithAI&lt;/p&gt;

&lt;p&gt;Missed Part 1? &lt;a href="https://shipwithai.io/blog/loop-engineering-from-harness-to-loops/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=loop-engineering&amp;amp;utm_content=part-2-prev" rel="noopener noreferrer"&gt;Loop Engineering: Why You Should Stop Prompting&lt;/a&gt;. Part 3 — &lt;em&gt;Stop Conditions: Making "Done" Mean Something&lt;/em&gt; — ships next.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Prompting, Start Designing Loops: The Fourth Era of AI Coding</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Wed, 19 Aug 2026 01:00:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/stop-prompting-start-designing-loops-the-fourth-era-of-ai-coding-4e5k</link>
      <guid>https://dev.to/shipwithaiio/stop-prompting-start-designing-loops-the-fourth-era-of-ai-coding-4e5k</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Prompting turn-by-turn is now the bottleneck, not the skill. A loop replaces it with three things: a recursive goal, a &lt;em&gt;verifiable&lt;/em&gt; stop condition, and unsupervised iteration. Is it a rebranded cronjob? Mostly yes — except for the one organ cron doesn't have. Before you build one, run the six-item harness checklist at the bottom.&lt;/p&gt;

&lt;p&gt;This is a summary of Part 1 of the 9-part &lt;strong&gt;Loop Engineering&lt;/strong&gt; series on ShipWithAI. &lt;a href="https://shipwithai.io/blog/loop-engineering-from-harness-to-loops/" rel="noopener noreferrer"&gt;Read the full article →&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Four eras, and each one moved the skill upward
&lt;/h2&gt;

&lt;p&gt;Every shift in AI-assisted coding moved the unit of leverage one layer up the stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Era&lt;/th&gt;
&lt;th&gt;Unit of leverage&lt;/th&gt;
&lt;th&gt;Your job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Words in one message&lt;/td&gt;
&lt;td&gt;Phrase the request well&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What the agent reads&lt;/td&gt;
&lt;td&gt;Curate CLAUDE.md, docs, memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Harness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent's runtime&lt;/td&gt;
&lt;td&gt;Build guardrails, hooks, verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Loops&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The system running the agent&lt;/td&gt;
&lt;td&gt;Define the goal + the stop condition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important part: loops do &lt;strong&gt;not&lt;/strong&gt; replace harnesses. They wrap them. The harness was the floor — a loop is that floor running on a timer, feeding itself.&lt;/p&gt;

&lt;p&gt;Which also means the failure mode is obvious in hindsight. The same loop that multiplies a careful engineer's output multiplies a careless engineer's slop.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually counts as a loop
&lt;/h2&gt;

&lt;p&gt;The canonical definition has three parts, and dropping any one of them gets you something else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A recursive goal&lt;/strong&gt; — a stated purpose with observable progress&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A verifiable stop condition&lt;/strong&gt; — checkable by a machine &lt;em&gt;or by a different model&lt;/em&gt;, never by the worker's own say-so&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsupervised iteration&lt;/strong&gt; — it runs without you prompting each cycle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Matt Van Horn's plain version is the one worth memorizing: &lt;em&gt;cron plus a decision-maker in the body.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  "It's just a cronjob with a rebrand"
&lt;/h2&gt;

&lt;p&gt;The honest answer is &lt;em&gt;mostly yes&lt;/em&gt; — the original essay concedes this up front rather than swatting it away, because the minimal viable loop genuinely does look like one. Geoffrey Huntley's Ralph, from July 2025:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; :&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;PROMPT.md | claude-code&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. So where's the difference?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cron&lt;/th&gt;
&lt;th&gt;Loop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Fixed script on a schedule&lt;/td&gt;
&lt;td&gt;Adaptive iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Termination&lt;/td&gt;
&lt;td&gt;Exits when the script finishes&lt;/td&gt;
&lt;td&gt;Exits when a condition is &lt;strong&gt;verified&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judging "done"&lt;/td&gt;
&lt;td&gt;None — implicit in script exit&lt;/td&gt;
&lt;td&gt;A decision-maker evaluates it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The decision-maker is the whole ballgame — the skeptics are right about everything except that one organ. It reads current state, picks the next action, and judges completion. &lt;strong&gt;Cron cannot recognize success. A loop is built on recognizing it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The anti-pattern: "run it day and night until the feature is done"
&lt;/h2&gt;

&lt;p&gt;This sounds ambitious. It is actually the single most reliable way to get fake-done work.&lt;/p&gt;

&lt;p&gt;Anthropic's November 2025 guidance on long-running agents flagged exactly this: agents without a verifiable stop declare victory prematurely and hand you features that are confidently broken.&lt;/p&gt;

&lt;p&gt;Compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;"Run until done"&lt;/strong&gt; — subjective, unbounded, unfalsifiable&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;"Iterate until the failing test passes, capped at N runs"&lt;/strong&gt; — machine-checkable and bounded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is a stop condition &lt;em&gt;someone other than the worker&lt;/em&gt; can check — a test, a lint gate, a schema, or a separate model grading the result.&lt;/p&gt;

&lt;p&gt;One nuance people get backwards: &lt;strong&gt;the caps are not the stop condition.&lt;/strong&gt; Iterations, cost, and wall-clock time are safety rails. The condition is the goal. If your only stop is &lt;code&gt;max_iterations&lt;/code&gt;, you don't have a loop — you have a budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the engineer's job goes
&lt;/h2&gt;

&lt;p&gt;What disappears: typing "fix the tests again" for the fourth time.&lt;/p&gt;

&lt;p&gt;What doesn't disappear — and gets &lt;em&gt;more&lt;/em&gt; important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verification stays yours to design.&lt;/strong&gt; No loop should grade its own homework — that's the failure mode every source in the essay converges on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop design assumes a working harness underneath it.&lt;/strong&gt; Skip that and you've automated your weaknesses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The original piece owns up to this with a first-person example: a maker agent in &lt;strong&gt;ShipWithAI's own content pipeline&lt;/strong&gt; fabricated a plausible CEO quote, passed its own self-check, and was only caught by an independent fact-checker pass before publish. Small blast radius, same organ failure.&lt;/p&gt;

&lt;p&gt;Boris Cherny — who created Claude Code, deleted his IDE in November 2025, and shipped 259 PRs in 30 days with 100% written by Claude Code — runs a recurring &lt;code&gt;/loop 5m /babysit&lt;/code&gt; command alongside explicit self-verification. His rule: &lt;em&gt;give Claude a way to verify its work&lt;/em&gt;, credited with a 2–3x quality difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  The six-item harness readiness checklist
&lt;/h2&gt;

&lt;p&gt;Run this against your repo before you write a single loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Conventions written down&lt;/strong&gt; — do they live in CLAUDE.md/skills, or only in your head?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-negotiables are mechanical&lt;/strong&gt; — do hooks enforce the rules, or do you catch violations in review?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Done" is machine-checkable&lt;/strong&gt; — is there a failing test, lint gate, or schema check?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State survives dead sessions&lt;/strong&gt; — does progress persist in files the next run reads?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two agents can share the repo&lt;/strong&gt; — can they work without colliding (worktrees)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known caps&lt;/strong&gt; — can you name your iteration, cost, and wall-clock limits?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Scoring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Items 1–3 fail&lt;/strong&gt; → build the harness first. A loop will only automate what's already weak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Items 1–3 pass, 4–6 fail&lt;/strong&gt; → you're exactly where the series picks up. Parts 4–6 cover state, parallel agents, and caps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worth being strict here: the source's bar for actually running a loop is all three gates &lt;em&gt;plus&lt;/em&gt; all six checks — the task repeats, "done" is machine-checkable, and the harness clears the list.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;The idea isn't new, it just converged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ReAct (2022)&lt;/strong&gt; — formalized reason-plus-action academically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AutoGPT (2023)&lt;/strong&gt; — shipped the loop without verification; the failures were instructive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ralph (July 2025)&lt;/strong&gt; — minimal viable loop, fresh context each iteration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt; (Spring 2026)&lt;/strong&gt; — productized verifiable stop conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration loops (June 2026)&lt;/strong&gt; — loops supervising other loops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simon Willison named the skill in September 2025, months ahead of the wave; Addy Osmani's &lt;em&gt;Loop Engineering&lt;/em&gt; essay is the synthesis the original leans on for the term. Peter Steinberger's June 7 post — &lt;em&gt;stop prompting agents, start designing loops&lt;/em&gt; — reached roughly 2.2 million viewers and split the room between people who agreed without a definition and people calling it cronjobs with a rebrand.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run the six-item checklist against one repo&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;one&lt;/strong&gt; task where "done" is already machine-checkable&lt;/li&gt;
&lt;li&gt;Don't build yet — Part 2 covers the anatomy before the assembly&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;This is a condensed summary.&lt;/strong&gt; The full article covers the era-by-era breakdown, the complete steelman of the cron objection, and the self-verification discipline in depth:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://shipwithai.io/blog/loop-engineering-from-harness-to-loops/" rel="noopener noreferrer"&gt;Loop Engineering: Why You Should Stop Prompting&lt;/a&gt;&lt;/strong&gt; — Part 1 of 9, ShipWithAI&lt;/p&gt;

&lt;p&gt;The rest of the series: Part 2 loop anatomy, Part 3 stop conditions (the hardest part to get right), Parts 4–6 state, parallel agents and caps, Part 7 documented real-world failures.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>We Wrote 8 Posts on Claude Code Harness Engineering. Then We Automated Our Own Advice</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:15:08 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/we-wrote-8-posts-on-claude-code-harness-engineering-then-we-automated-our-own-advice-2d9c</link>
      <guid>https://dev.to/shipwithaiio/we-wrote-8-posts-on-claude-code-harness-engineering-then-we-automated-our-own-advice-2d9c</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;We wrote eight posts on Claude Code harness engineering — Memory, Tools, Permissions, Hooks, Observability. Then we spent every new project rebuilding that harness by hand, and so did every teammate, slightly differently. A discipline you re-type every Monday isn't a discipline; it's a chore. So we turned the series into one open-source command.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Monday that broke the pattern
&lt;/h2&gt;

&lt;p&gt;New repo. Empty &lt;code&gt;.claude/&lt;/code&gt; folder. Cursor blinking in a fresh &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I did what I'd done a dozen times before: opened last month's project in a second window and started copying. The permissions block. The &lt;code&gt;rm -rf&lt;/code&gt; guard. The &lt;code&gt;MEMORY.md&lt;/code&gt; scaffold. The MCP config. Paste, find-and-replace the project name, tab back.&lt;/p&gt;

&lt;p&gt;Halfway through, the irony landed. We had just published eight posts teaching developers exactly this — how to build a reliable harness, layer by layer. And here I was, the author, retyping it from memory and hoping I hadn't skipped a layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discipline had no tooling
&lt;/h2&gt;

&lt;p&gt;The pillar idea of the whole series is one equation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent = Model + Harness&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model is a commodity — everyone on the same Claude version gets the same raw capability. The harness is the part that's &lt;em&gt;yours&lt;/em&gt;. It's why one team ships clean while the team next door ships rollback after rollback.&lt;/p&gt;

&lt;p&gt;We broke it into five layers, one deep dive each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — what it knows before you type. &lt;code&gt;CLAUDE.md&lt;/code&gt; as a &lt;em&gt;failure log&lt;/em&gt;, not a wishlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — what it can reach, through MCP servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt; — what it's allowed to touch. Thirty seconds of config stops a careless install from wrecking your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks&lt;/strong&gt; — what's enforced at runtime. The only layer context can't argue its way out of.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — what you can see afterward, plus a loop that makes the agent verify its own work before claiming it's done.
Under all of it sits the constraint paradox: &lt;strong&gt;the more you restrict what the agent can do, the better it does what it should.&lt;/strong&gt; LangChain made the same case publicly — harness and context changes, not a model swap, drove a +13.7-point jump (52.8% → 66.5%) on Terminal Bench 2.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good theory. Real evidence. And every post ended the same way: &lt;em&gt;"copy this block into your &lt;code&gt;.claude/&lt;/code&gt; folder."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We'd written a beautiful manual for a factory, then asked everyone to assemble the production line by hand in every new building. An SOP you execute manually every time isn't a system. It's a memory test you eventually fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two failure modes nobody warns you about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You forget a layer.&lt;/strong&gt; Never the famous ones — you'll always remember the &lt;code&gt;rm -rf&lt;/code&gt; hook because it scared you once. You forget the boring one: the observability log, the &lt;code&gt;MEMORY.md&lt;/code&gt; index. And you don't notice until three weeks in, when the agent reintroduces a bug you already fixed in a session it has no record of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your team drifts before the project starts.&lt;/strong&gt; One dev writes a strict permissions block. Another copies an older, looser one. A third skips hooks "just for now." Now you don't have a harness — you have four dialects of one. The point of version-controlling it is that the team inherits the same reliability. Hand-assembly defeats that on day one.&lt;/p&gt;

&lt;p&gt;The fix isn't a smarter model. It's making the right harness the &lt;em&gt;easy&lt;/em&gt; harness: reproducible, identical, boring to set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  So we turned the series into one command
&lt;/h2&gt;

&lt;p&gt;We took every post and asked: what if this weren't a code block to copy, but a question in an interview?&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;shipwithai-starter&lt;/strong&gt; — one open-source plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/shipwithai-starter:init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks about your stack and how much rigor you want, then writes the whole harness — five minutes for the essentials, thirty for the full setup. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add CLAUDE.md .claude/ .mcp.json docs/
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: add Claude Code harness"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your teammate clones the repo, opens Claude Code, and the harness is already there. No second window. No drift. The production line ships with the building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdso54vgaip312slbm55b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdso54vgaip312slbm55b.webp" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every post now has a command on the other side of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you read&lt;/th&gt;
&lt;th&gt;What you can now run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The &lt;code&gt;MEMORY.md&lt;/code&gt; fix · failure-log &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/setup-memory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP setup guide&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/setup-mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30-second install security&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/setup-permissions&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which hook do you need&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/setup-hooks&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-verification loop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/setup-observability&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The blog still teaches you to do it by hand — and you should read it, because you can't maintain what you don't understand. The plugin just means you do it by hand &lt;em&gt;once&lt;/em&gt;, to learn it, instead of every Monday, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two layers we haven't written about yet
&lt;/h2&gt;

&lt;p&gt;Honest part: the series says five layers. The plugin sets up &lt;strong&gt;seven&lt;/strong&gt;. That's what dogfooding does to a framework.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt; — sub-agents in &lt;code&gt;.claude/agents/&lt;/code&gt;, including a drift-monitor that runs weekly and tells you when your &lt;code&gt;CLAUDE.md&lt;/code&gt; has quietly stopped matching the code. The failure-log pattern, automated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSOT&lt;/strong&gt; — single-source-of-truth docs: architecture file, ADRs, codebase maps. The context that keeps the agent from re-litigating decisions you already made.
We'll write the deep dives. But we'd rather ship the tool and admit the framework grew &lt;em&gt;because we used it&lt;/em&gt; than pretend the map was perfect before we walked the territory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Read every line before you run it
&lt;/h2&gt;

&lt;p&gt;Fully open source: &lt;strong&gt;&lt;a href="https://github.com/ShipWithAI/shipwithai-plugins" rel="noopener noreferrer"&gt;github.com/ShipWithAI/shipwithai-plugins&lt;/a&gt;&lt;/strong&gt; — hooks, interview logic, templates, the drift-monitor agent, all in plain Markdown and Python. Audit the thing that's about to write to your &lt;code&gt;.claude/&lt;/code&gt; folder. That's the only honest way to ship a tool whose entire job is enforcing constraints.&lt;/p&gt;

&lt;p&gt;Then run &lt;code&gt;/shipwithai-starter:init&lt;/code&gt; on a real project, commit it, and have a teammate pull. The first time someone else's harness shows up identical to yours with zero setup, you'll feel the difference the whole series was pointing at.&lt;/p&gt;

&lt;p&gt;The real test: open a new repo six months from now. If you catch yourself copying your harness out of an old project by hand, the tooling lost. If it's already there because one command and one commit put it there — that's harness engineering finally engineering itself.&lt;/p&gt;

&lt;p&gt;We wrote eight posts about the discipline. This is the ninth, and it's the one where we stopped practicing it by hand.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;shipwithai-starter is built and maintained by the ShipWithAI team. We use it on our own repos before we recommend it on yours.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Review AI-generated Pull Requests in 6 Steps with Claude Code</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Tue, 26 May 2026 13:59:16 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/how-to-review-ai-generated-pull-requests-in-6-steps-with-claude-code-3ie9</link>
      <guid>https://dev.to/shipwithaiio/how-to-review-ai-generated-pull-requests-in-6-steps-with-claude-code-3ie9</guid>
      <description>&lt;h1&gt;
  
  
  How to Review AI-generated Pull Requests in 6 Steps with Claude Code
&lt;/h1&gt;

&lt;p&gt;When I started seeing three AI-written PRs land in my inbox every hour, my old checklist fell apart. The diff looked clean, the CI was green, and the commit messages were nicely phrased. Yet every PR slipped a subtle bug past me - a mock-heavy test, a wrong API signature, or a side-effect hidden at import time. After a couple of production incidents I built a linear, single-pass checklist that catches the six error families that coding agents (Claude Code, Cursor, Codex, etc.) tend to introduce. Below is the exact workflow I now run on every AI-authored PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;At least a year of code-review experience (comfortable with &lt;code&gt;git diff&lt;/code&gt;, GitHub PR UI, and local test runs).&lt;/li&gt;
&lt;li&gt;Have used a coding agent that can generate a diff (Claude Code is the reference).&lt;/li&gt;
&lt;li&gt;The repo must have a test runner you can invoke locally or via CI.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1 - Write the Expected Scope &lt;em&gt;before&lt;/em&gt; opening the diff
&lt;/h2&gt;

&lt;p&gt;Before you click &lt;strong&gt;Files changed&lt;/strong&gt;, open the PR metadata and write a one- or two-sentence scope that defines exactly what the PR is allowed to touch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Grab the title and body for quick copy-paste&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;view 1234 &lt;span class="nt"&gt;--json&lt;/span&gt; title,body,headRefName
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refactor: extract auth helper"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Extract `verifyJwt` from `auth/handler.ts` into `auth/jwt.ts`. No behavior change."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headRefName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"feat/extract-auth-helper"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that I write the explicit expectation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add `verifyJwt` in `auth/jwt.ts`, update import in `auth/handler.ts`, no other files change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt; - if you read the diff first you're prone to rationalising "oh, the extra files look okay". With a concrete anchor you can quickly filter the diff later and spot scope drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 - Detect Fake Tests and Over-mocking
&lt;/h2&gt;

&lt;p&gt;Agents love to make the CI green by writing tests that mock everything. A quick way to expose a &lt;em&gt;fake&lt;/em&gt; test is to flip one assertion and see if the test still passes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pick a representative test and invert an assertion&lt;/span&gt;
pytest tests/test_jwt.py::test_verify_valid_token &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;--tb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the test still passes after you changed &lt;code&gt;assertEqual&lt;/code&gt; to &lt;code&gt;assertNotEqual&lt;/code&gt;, the test isn't exercising production code at all.&lt;/p&gt;

&lt;p&gt;Next, audit the mock-to-assert ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Count mock usage&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"mock&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;patch&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;MagicMock"&lt;/span&gt; tests/test_jwt.py
&lt;span class="c"&gt;# Count assertions&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"assert"&lt;/span&gt; tests/test_jwt.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ratio greater than &lt;strong&gt;2:1&lt;/strong&gt; (mocks per assertion) is a red flag - the test is basically asserting that the mock was called with the arguments the mock itself supplied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix path&lt;/strong&gt; - If you find a fake test, comment with a concrete example of a real assertion (e.g., "assert that &lt;code&gt;verifyJwt&lt;/code&gt; returns a decoded payload for a real token") and ask the author to add it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 - Verify API Call Signatures
&lt;/h2&gt;

&lt;p&gt;For each new or changed function call, jump to the definition and compare the signature with the official library docs, not just the local stub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example of a subtle bug the type-checker missed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local stub shows &lt;code&gt;db.query(sql: string, params: any[], callback: fn): void&lt;/code&gt;. Because it returns &lt;code&gt;void&lt;/code&gt;, the &lt;code&gt;await&lt;/code&gt; results in &lt;code&gt;undefined&lt;/code&gt;. In production this means the caller always receives &lt;code&gt;undefined&lt;/code&gt; and may silently skip error handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check&lt;/strong&gt; - Open the library's README or the version-pinned docs in &lt;code&gt;package.json&lt;/code&gt;/&lt;code&gt;requirements.txt&lt;/code&gt; and ensure the signature matches. Run a quick integration test with edge inputs (e.g., &lt;code&gt;null&lt;/code&gt; query, empty param array) to confirm runtime behaviour.&lt;/p&gt;

&lt;p&gt;If the signature is wrong, a single-line comment with a link to the correct docs and a suggested fix is usually enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 - Confirm the Diff stays inside the declared scope
&lt;/h2&gt;

&lt;p&gt;Now that you have a concrete scope string, filter the diff stat and list any files that fall outside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--stat&lt;/span&gt; origin/main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^ auth/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; src/utils/logger.ts        |  12 ++++++------
 src/api/users/handler.ts   |  34 ++++++++++++++--------
 2 files changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both files are outside the expected &lt;code&gt;auth/&lt;/code&gt; folder. Look at the commit messages for those files - if they say "while we're here, tidy logger", that's &lt;em&gt;scope drift&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action&lt;/strong&gt; - Request a split of the PR. Do not accept a "just a small cleanup" justification; the extra files are review surface you never loaded into context, and hidden bugs often hide there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 - Hunt for Hallucinated Imports and Hidden Side-effects
&lt;/h2&gt;

&lt;p&gt;Two quick commands catch the most common issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify the import resolves to the expected module&lt;/span&gt;
python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from utils.security import sanitize_html; print(sanitize_html.__module__)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the import resolves to an unexpected module (e.g., a local &lt;code&gt;utils/__init__.py&lt;/code&gt; re-exports &lt;code&gt;bleach.sanitizer.sanitize_html&lt;/code&gt;), you have a hallucinated import - the code compiles but does nothing at runtime.&lt;/p&gt;

&lt;p&gt;Next, look for top-level side-effects that will run on import.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Grep for network or file I/O at module level&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"requests.get&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;open(&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;os&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;environ&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;subprocess"&lt;/span&gt; &lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find a call like &lt;code&gt;requests.get(URL)&lt;/code&gt; at the top of a module, run the test suite with networking disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--disable-socket&lt;/span&gt; tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the suite still passes, the import-time network call is being swallowed by a mock - a classic hidden side-effect that will break in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation&lt;/strong&gt; - Ask the author to move the call into a function or guard it with &lt;code&gt;if __name__ == "__main__":&lt;/code&gt; and ensure a real test exercises the code path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 - Align Commit Messages with the Diff
&lt;/h2&gt;

&lt;p&gt;Pick a random commit and compare its message with the actual changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show concise log for the PR range&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; origin/main..HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a1b2c3d fix: null check in verifyJwt
e4f5g6h refactor: extract auth helper
i7j8k9l test: add jwt verification cases
m0n1o2p chore: update package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show the diff for the selected commit&lt;/span&gt;
git show a1b2c3d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the diff touches a retry policy, timeout adjustments, and a null check, but the message only mentions the null check, the message covers less than 20 % of the change - a &lt;em&gt;commit-message mismatch&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - The diff should be explainable by the message within a ±20 % margin. If it isn't, request a rewrite of the commit (or a squash-and-rebase) so the history stays trustworthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision: Ship or Reject?
&lt;/h2&gt;

&lt;p&gt;After the six checks, apply the following matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failed step&lt;/th&gt;
&lt;th&gt;Can a one-sentence nudge fix it?&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 - Scope not declared&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Comment: add concrete scope line.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 - Fake test&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reject&lt;/strong&gt; - attach the failing inverted-assertion example.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 - Wrong API signature&lt;/td&gt;
&lt;td&gt;✅ (if isolated)&lt;/td&gt;
&lt;td&gt;Nudge with docs link; reject if pervasive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 - Scope drift&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reject&lt;/strong&gt; - ask for split PR.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 - Hallucinated import / hidden side-effect&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reject&lt;/strong&gt; - include verification command output.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 - Misleading commit message&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reject&lt;/strong&gt; - require rewrite.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The guiding principle is binary: either the PR can be shipped after a quick nudge, or it's rejected with a concrete, actionable comment. "Comment and forget" leads to half-reviewed PRs that linger forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write a concrete scope first&lt;/strong&gt; - it prevents confirmation bias when you later look at the diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flip one assertion&lt;/strong&gt; - a simple fail-fast check for fake tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit mocks vs. asserts&lt;/strong&gt; - a high ratio signals over-mocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check signatures against official docs&lt;/strong&gt;, not just the local stub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter diff by the declared scope&lt;/strong&gt;; any out-of-scope file should trigger a split request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify imports resolve to the intended module&lt;/strong&gt; and that no top-level I/O runs on import.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit messages must map to the diff within ~20 %&lt;/strong&gt;; otherwise reject.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running this checklist takes about &lt;strong&gt;10-12 minutes&lt;/strong&gt; for a typical 4-commit PR and catches at least four of the six error families that a human-only checklist would miss. It's a small habit shift that saves hours of post-merge fire-fighting.&lt;/p&gt;




&lt;p&gt;If you find this workflow useful, we packaged it into a Claude Code plugin that automates the repetitive parts (scope extraction, mock-audit, import verification). Feel free to check it out if you want the automated version.&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://shipwithai.io/blog/vi/reviewing-ai-generated-pull-requests-2026-part1" rel="noopener noreferrer"&gt;https://shipwithai.io/blog/vi/reviewing-ai-generated-pull-requests-2026-part1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>sdlc</category>
      <category>github</category>
    </item>
    <item>
      <title>Why Your AI Coach’s Warmth Might Be Hiding a Critical Regression</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Fri, 22 May 2026 16:03:51 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/why-your-ai-coachs-warmth-might-be-hiding-a-critical-regression-4nf9</link>
      <guid>https://dev.to/shipwithaiio/why-your-ai-coachs-warmth-might-be-hiding-a-critical-regression-4nf9</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;When Claude Opus upgraded last quarter, our CSAT jumped four points and active conversations were up 11%. The VP called it the cleanest upgrade of the year—until we noticed the coach stopped saying &lt;em&gt;“let's revisit this plan.”&lt;/em&gt; That drop was half the size of the CSAT gain and signaled a hidden regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: sycophancy
&lt;/h2&gt;

&lt;p&gt;Anthropic’s May 2026 audit calls the “overly human” vibe &lt;em&gt;sycophancy&lt;/em&gt;: the model agrees or validates the user even when the correct move is to disagree. The study measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;9 % overall guidance chats&lt;/li&gt;
&lt;li&gt;25 % on relationship advice&lt;/li&gt;
&lt;li&gt;38 % on spirituality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For decision‑support features, useful disagreement is a load‑bearing metric. When a model becomes too agreeable, the dashboard shows higher warmth but the recommendation quality stalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete technique: the pushback eval
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Collect failure modes&lt;/strong&gt; – pull the top three user logs where the feature should have pushed back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write 30 adversarial prompts&lt;/strong&gt; – each prompt asks the model to evaluate a risky plan or contradictory statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; – simple yes/no rubric: &lt;em&gt;Did the model refuse or suggest a different course?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run on every model bump&lt;/strong&gt; – record the pushback rate and baseline it against the previous version.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spreadsheet is enough; data‑science can later automate it. When Opus 4.7 shipped, the relationship sycophancy rate halved, and the pushback eval caught a 12 % dip in decision‑support recommendations that otherwise would have gone unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Warmth metrics (CSAT, engagement) can mask regression in useful disagreement.&lt;/li&gt;
&lt;li&gt;Track a &lt;em&gt;pushback rate&lt;/em&gt; alongside satisfaction.&lt;/li&gt;
&lt;li&gt;A 30‑prompt adversarial sheet costs an afternoon and saves a quarter of a product’s ROI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Action
&lt;/h2&gt;

&lt;p&gt;Pick one move this sprint: add pushback rate to your eval dashboard, re‑run the sheet on the next model upgrade, or present the warmth vs. pushback delta at your QBR. The metric will surface hidden regressions before they cost you a feature.&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://shipwithai.io/blog/en/claude-opus-overly-human-behavior" rel="noopener noreferrer"&gt;https://shipwithai.io/blog/en/claude-opus-overly-human-behavior&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>sdlc</category>
      <category>productmanagement</category>
    </item>
    <item>
      <title>The Complete Claude Code Harness Engineering Guide (5 Layers, 8 Deep-Dives)</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Fri, 08 May 2026 01:00:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/the-complete-claude-code-harness-engineering-guide-5-layers-8-deep-dives-3d4j</link>
      <guid>https://dev.to/shipwithaiio/the-complete-claude-code-harness-engineering-guide-5-layers-8-deep-dives-3d4j</guid>
      <description>&lt;p&gt;Harness engineering is everything around your AI agent except the model: memory, tools, permissions, hooks, observability. LangChain gained 13.7 benchmark points changing only the harness. This guide is a curated reading path, organized by layer, with a deep-dive post for every part of a Claude Code harness.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1 only (what most devs have)
  → Advice the model may ignore

All 5 layers (Memory → Tools →
  → Enforcement the model
  Permissions → Hooks → Observability)
    cannot bypass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LangChain jumped from 52.8% to 66.5% on Terminal Bench 2.0 by changing only the harness. Same model. 13.7 points of pure architecture gain (&lt;a href="https://blog.langchain.com/improving-deep-agents-with-harness-engineering/" rel="noopener noreferrer"&gt;LangChain Blog, Feb 2026&lt;/a&gt;). Most Claude Code users stop at Layer 1. This guide is the reading path to the other four.&lt;/p&gt;

&lt;p&gt;If you want the &lt;em&gt;theory&lt;/em&gt; of harness engineering, read the &lt;a href="https://shipwithai.io/blog/harness-engineering-claude-code/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-harness-engineering-claude-code" rel="noopener noreferrer"&gt;pillar post&lt;/a&gt;. If you want the &lt;em&gt;architecture&lt;/em&gt; deep-dive, read the &lt;a href="https://shipwithai.io/blog/claude-code-harness-5-layers/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-harness-5-layers" rel="noopener noreferrer"&gt;5 layers post&lt;/a&gt;. This post is something different: a navigation hub organized by layer, with one deep-dive per topic, that you can return to as your harness grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Claude Code harness engineering?
&lt;/h2&gt;

&lt;p&gt;Harness engineering is the discipline of building everything around an AI agent — constraints, tools, feedback loops, observability — so it becomes reliable in production. For Claude Code, the harness is five layers: Memory (CLAUDE.md), Tools (MCP), Permissions (settings.json), Hooks (PreToolUse/PostToolUse), and Observability (session logs).&lt;/p&gt;

&lt;p&gt;The formula: &lt;strong&gt;Agent = Model + Harness&lt;/strong&gt; (&lt;a href="https://martinfowler.com/articles/harness-engineering.html" rel="noopener noreferrer"&gt;Martin Fowler, Apr 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The model is commodity. Every team on Sonnet 4.6 or Opus 4.7 gets the same raw capability. Your harness is what differentiates your team's output.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the 5 layers of a Claude Code harness?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Claude Code File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Memory&lt;/td&gt;
&lt;td&gt;What the agent knows&lt;/td&gt;
&lt;td&gt;CLAUDE.md, MEMORY.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Tools&lt;/td&gt;
&lt;td&gt;What it can reach&lt;/td&gt;
&lt;td&gt;settings.json (MCP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Permissions&lt;/td&gt;
&lt;td&gt;What it's allowed to do&lt;/td&gt;
&lt;td&gt;settings.json allow/deny&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Hooks&lt;/td&gt;
&lt;td&gt;What's enforced at runtime&lt;/td&gt;
&lt;td&gt;PreToolUse/PostToolUse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Observability&lt;/td&gt;
&lt;td&gt;What you can see afterward&lt;/td&gt;
&lt;td&gt;Session logs, cost tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Layer 1: What does your agent know before you type?
&lt;/h2&gt;

&lt;p&gt;The memory layer is every file Claude Code reads before the first keystroke. CLAUDE.md holds your project rules. MEMORY.md holds the evolving state. Most developers ship only a CLAUDE.md and treat it as a wishlist of aspirations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shipwithai.io/blog/claude-code-memory-md-fix/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-memory-md-fix" rel="noopener noreferrer"&gt;Your AI Agent Forgets Everything. Here's the Fix.&lt;/a&gt;&lt;/strong&gt; — MEMORY.md is a 200-line index that Claude reads at session start. Setup takes 5 minutes. Read this first if you keep re-explaining the same architecture decisions every Monday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shipwithai.io/blog/claude-md-failure-log-pattern/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-md-failure-log-pattern" rel="noopener noreferrer"&gt;Your CLAUDE.md Is an Instruction File. It Should Be a Failure Log.&lt;/a&gt;&lt;/strong&gt; — Mitchell Hashimoto's AGENTS.md in Ghostty has zero aspirational lines. Every entry traces to a real agent mistake. The post includes the Failure-to-Constraint Decision Tree: dangerous actions go to Hooks, repeatable workflows go to Commands, style goes to CLAUDE.md.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4: What can the agent NOT do?
&lt;/h2&gt;

&lt;p&gt;Hooks are the enforcement layer. Memory is advice. Hooks are law. A PreToolUse hook that exits with code 2 blocks Claude Code from running a command, full stop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PreToolUse hook: 6 lines that save you from yourself&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOOL_INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"DROP TABLE"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BLOCKED: destructive SQL in production"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://shipwithai.io/blog/claude-code-hook-decision-guide/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-hook-decision-guide" rel="noopener noreferrer"&gt;Which Claude Code Hook Do You Need? A Decision Guide&lt;/a&gt;&lt;/strong&gt; — The 4 handler types (Deny, Log, Transform, Enrich), when to reach for PreToolUse vs PostToolUse, and which 3 hooks every production setup should have.&lt;/p&gt;

&lt;p&gt;A PreToolUse hook exiting with code 2 is the only mechanism in Claude Code that unconditionally blocks a tool call. Instructions in CLAUDE.md can still be overridden by context or model reasoning. Hooks cannot be bypassed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 5: How do you know what your agent actually did?
&lt;/h2&gt;

&lt;p&gt;Observability turns "my agent did something weird" into a reproducible bug report. One of LangChain's three harness improvements was a verification middleware that made the agent check its own work before marking a task complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shipwithai.io/blog/claude-code-self-verification-loop/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-self-verification-loop" rel="noopener noreferrer"&gt;Build a Self-Verification Loop for Claude Code&lt;/a&gt;&lt;/strong&gt; — Adapts LangChain's PreCompletionChecklistMiddleware to Claude Code. Boris Cherny (creator of Claude Code) calls verification "probably the most important thing" for quality.&lt;/p&gt;

&lt;p&gt;LangChain's three improvements mapped to layers: context injection (Layer 1), self-verification loops (Layer 5), and compute allocation (Layer 5). No single layer explained the full +13.7 point gain. They needed three layers working together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does this actually work?
&lt;/h2&gt;

&lt;p&gt;Three independent data points prove constraints beat capability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LangChain&lt;/strong&gt;: +13.7 on Terminal Bench 2.0 with harness changes only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Codex&lt;/strong&gt;: ~1 million lines of production code, zero human-written lines over five months, all inside heavily constrained harness environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mitchell Hashimoto's Ghostty&lt;/strong&gt;: every AGENTS.md line is a prevented failure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shipwithai.io/blog/harness-engineering-constraint-paradox/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-harness-engineering-constraint-paradox" rel="noopener noreferrer"&gt;The Constraint Paradox: Less AI Freedom, Better Code&lt;/a&gt;&lt;/strong&gt; — Breaks down all three data points with benchmark tables and the counterintuitive finding that running at maximum reasoning budget scored &lt;em&gt;worse&lt;/em&gt; (53.9%) than high (63.6%). Read this when someone says "we just need a smarter model."&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does this matter for your career?
&lt;/h2&gt;

&lt;p&gt;84% of developers use AI tools. Only 29% trust the output. That 55-point gap is the senior engineer's new job. One harness committed to version control multiplies across your whole team. Writing a great CLAUDE.md for 10 developers pays off more than writing 10,000 lines of code yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shipwithai.io/blog/harness-engineering-senior-developer-guide/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-harness-engineering-senior-developer-guide" rel="noopener noreferrer"&gt;Senior Engineers Don't Write Code. They Build Harnesses.&lt;/a&gt;&lt;/strong&gt; — The career case with a harness review checklist for your next PR and the 4-era evolution of where senior engineers add value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where should you start reading?
&lt;/h2&gt;

&lt;p&gt;Three paths based on where you are today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New to harness engineering.&lt;/strong&gt; Start with the &lt;a href="https://shipwithai.io/blog/harness-engineering-claude-code/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-harness-engineering-claude-code" rel="noopener noreferrer"&gt;pillar post&lt;/a&gt; for the definition, then the &lt;a href="https://shipwithai.io/blog/claude-code-harness-5-layers/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-harness-5-layers" rel="noopener noreferrer"&gt;5 layers post&lt;/a&gt; for the architecture. Come back here for your next deep-dive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have a CLAUDE.md and want more rigor.&lt;/strong&gt; Read &lt;a href="https://shipwithai.io/blog/claude-code-memory-md-fix/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-memory-md-fix" rel="noopener noreferrer"&gt;the memory fix post&lt;/a&gt; first to add MEMORY.md, then &lt;a href="https://shipwithai.io/blog/claude-md-failure-log-pattern/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-md-failure-log-pattern" rel="noopener noreferrer"&gt;the failure-log pattern&lt;/a&gt; to rewrite your existing CLAUDE.md. Those two posts cover all of Layer 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your agent has scared you at least once.&lt;/strong&gt; Skip to the &lt;a href="https://shipwithai.io/blog/claude-code-hook-decision-guide/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-hook-decision-guide" rel="noopener noreferrer"&gt;hook decision guide&lt;/a&gt; and ship one PreToolUse guard before your next session. Then read &lt;a href="https://shipwithai.io/blog/harness-engineering-constraint-paradox/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-harness-engineering-constraint-paradox" rel="noopener noreferrer"&gt;the constraint paradox&lt;/a&gt; for why this actually works.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Claude Code harness engineering?
&lt;/h3&gt;

&lt;p&gt;Harness engineering for Claude Code is configuring five layers around the model (Memory, Tools, Permissions, Hooks, Observability) to make the agent reliable in production. The model is commodity. The harness is your differentiator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need all 5 layers to start?
&lt;/h3&gt;

&lt;p&gt;No. Start with Memory (CLAUDE.md + MEMORY.md) and Hooks (one PreToolUse guard). Those two cover the most common failure modes. Add the rest as your team scales or when a specific incident motivates it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is harness engineering different from prompt engineering?
&lt;/h3&gt;

&lt;p&gt;Prompt engineering shapes what the agent tries. Context engineering shapes what the agent knows. Harness engineering shapes what the agent &lt;em&gt;can and cannot do&lt;/em&gt;, using enforcement (hooks, permissions) rather than suggestions (prompts).&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this only apply to Claude Code?
&lt;/h3&gt;

&lt;p&gt;The principles apply to any AI coding agent. The implementation details (CLAUDE.md, PreToolUse hooks, MCP config) are Claude Code-specific. Claude Code offers the most programmable harness surface in the market today.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it now:&lt;/strong&gt; Pick one path above, open the first linked post, copy one code block into your &lt;code&gt;.claude/&lt;/code&gt; folder, and run one Claude Code session with the change applied. The compound benefit starts on session #2.&lt;/p&gt;

&lt;p&gt;Which layer would you add first? Drop it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shipwithai.io/blog/claude-code-harness-engineering-guide/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-harness-engineering-guide" rel="noopener noreferrer"&gt;ShipWithAI&lt;/a&gt;. I write about Claude Code workflows, AI-assisted development, and shipping software faster with structured AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
    <item>
      <title>Hardening Your npm CI in 5 Concrete Layers</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Thu, 07 May 2026 14:20:33 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/hardening-your-npm-ci-in-5-concrete-layers-309f</link>
      <guid>https://dev.to/shipwithaiio/hardening-your-npm-ci-in-5-concrete-layers-309f</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Your CI pipeline installs dependencies far more often than any developer’s laptop. That frequency makes it the biggest npm attack surface. I recently saw the Bitwarden breach where a hijacked GitHub Action pulled a malicious CLI for 90 minutes and harvested every credential on the runner. Below is the exact 5‑layer playbook we dog‑fooded at ShipWithAI to stop that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most CI configs still look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;   &lt;span class="c1"&gt;# mutable tag&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt; &lt;span class="c1"&gt;# mutable tag&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;           &lt;span class="c1"&gt;# silent version bumps&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish&lt;/span&gt;           &lt;span class="c1"&gt;# uses stored NPM_TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The red flags are obvious: mutable tags, &lt;code&gt;npm install&lt;/code&gt;, long‑lived tokens, no lockfile validation, and no dependency review. Each one is a foothold for an attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1 – Enforce &lt;code&gt;npm ci&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;npm ci&lt;/code&gt; installs &lt;strong&gt;only&lt;/strong&gt; from the lockfile and fails on any mismatch. It also wipes &lt;code&gt;node_modules&lt;/code&gt; first, guaranteeing a clean slate. Replace every &lt;code&gt;npm install&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install deps&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci --ignore-scripts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit a project‑level &lt;code&gt;.npmrc&lt;/code&gt; with &lt;code&gt;ignore-scripts=true&lt;/code&gt;, &lt;code&gt;save-exact=true&lt;/code&gt;, and &lt;code&gt;audit-level=moderate&lt;/code&gt; so every runner inherits the same defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 – Validate lockfile integrity
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;lockfile-lint&lt;/code&gt; to the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint lockfile&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx lockfile-lint --allowed-hosts npmjs.com --validate-https&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This blocks PRs that tamper with the lockfile source URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3 – Dependency review action
&lt;/h3&gt;

&lt;p&gt;GitHub’s &lt;code&gt;dependency-review-action&lt;/code&gt; flags new or changed dependencies before merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dependency review&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/dependency-review-action@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;allow-scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runtime,development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 4 – Pin actions to SHA
&lt;/h3&gt;

&lt;p&gt;Instead of &lt;code&gt;actions/setup-node@v4&lt;/code&gt;, use the exact SHA of the release you’ve vetted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@d3b0c5f...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a tag gets hijacked, your workflow stays on the trusted commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 5 – OIDC trusted publishing
&lt;/h3&gt;

&lt;p&gt;Replace static &lt;code&gt;NPM_TOKEN&lt;/code&gt; secrets with OIDC tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm/publish-action@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;token-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oidc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub issues a short‑lived token that expires with the job, eliminating long‑lived credential leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Switching to &lt;code&gt;npm ci&lt;/code&gt; alone caught three silent version bumps in the first week. Adding the full stack stopped a malicious lockfile PR from ever reaching merge and removed the need to store a permanent NPM token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic installs&lt;/strong&gt; (&lt;code&gt;npm ci&lt;/code&gt;) are non‑negotiable for CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate lockfiles&lt;/strong&gt; before they touch the runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review deps&lt;/strong&gt; on every PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin actions&lt;/strong&gt; to immutable SHAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish with OIDC&lt;/strong&gt; to avoid static secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion &amp;amp; CTA
&lt;/h2&gt;

&lt;p&gt;These five layers are easy to copy‑paste into any repo and give you a solid defense against the kind of supply‑chain hijack that hit Bitwarden. Follow me for more concrete SDLC hardening tips and feel free to drop your CI questions in the comments.&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://shipwithai.io/blog/npm-ci-security-team-playbook/" rel="noopener noreferrer"&gt;https://shipwithai.io/blog/npm-ci-security-team-playbook/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>npm</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Which Claude Code Hook Do You Need? A Decision Guide</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Wed, 06 May 2026 01:00:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/which-claude-code-hook-do-you-need-a-decision-guide-21h5</link>
      <guid>https://dev.to/shipwithaiio/which-claude-code-hook-do-you-need-a-decision-guide-21h5</guid>
      <description>&lt;p&gt;Claude Code has 4 hook handler types (command, prompt, agent, http) and 21 lifecycle events. Most developers default to command hooks on PreToolUse. This decision guide helps you pick the right type for the right event, and tells you which 3 to implement first.&lt;/p&gt;




&lt;p&gt;Two configs. Same goal: block a force push to main. Different reliability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Command hook (deterministic, &amp;lt;5ms)&lt;/span&gt;
&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // empty'&lt;/span&gt; &amp;lt; /dev/stdin&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'git push.*(--force|-f).*main'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BLOCKED: force push to main"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Prompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(non-deterministic,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300-2000&lt;/span&gt;&lt;span class="err"&gt;ms)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Block this if it looks like a force push to a production branch"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command hook is 5 lines of bash. It runs in under 5ms. It catches every &lt;code&gt;git push --force main&lt;/code&gt; without exception.&lt;/p&gt;

&lt;p&gt;The prompt hook calls an LLM. It takes 300-2000ms. It might decide &lt;code&gt;--force-with-lease&lt;/code&gt; is safe enough to allow.&lt;/p&gt;

&lt;p&gt;Both are "hooks." Choosing the wrong type turns a guardrail into a suggestion. CLAUDE.md instructions achieve 70-90% compliance. Hooks achieve 100% — but only when you pick the right one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the 4 Claude Code hook handler types?
&lt;/h2&gt;

&lt;p&gt;Each type trades speed for intelligence differently. Pick the wrong type and your 100% guardrail drops to a probabilistic suggestion.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Handler&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Deterministic?&lt;/th&gt;
&lt;th&gt;Codebase Access?&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;&amp;lt;5ms&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (stdin only)&lt;/td&gt;
&lt;td&gt;Guardrails, formatting, logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt&lt;/td&gt;
&lt;td&gt;300-2000ms&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nuanced decisions on Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent&lt;/td&gt;
&lt;td&gt;2-10s&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (full tools)&lt;/td&gt;
&lt;td&gt;Deep verification, architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;50-500ms&lt;/td&gt;
&lt;td&gt;Yes (your server)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Team policies, centralized audit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Command hooks&lt;/strong&gt; are shell scripts. They read JSON from stdin, run fast, and return deterministic results. Use them for anything you can express as a string match, path check, or regex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt hooks&lt;/strong&gt; call an LLM to make a judgment call. Only use them when the decision genuinely requires reasoning, like evaluating subagent output quality on &lt;code&gt;SubagentStop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent hooks&lt;/strong&gt; spawn a full Claude Code session that can read files, search code, and run tools. Reserve them for verification tasks that need codebase context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP hooks&lt;/strong&gt; POST to your server. Useful for centralized team policies and audit logging.&lt;/p&gt;

&lt;p&gt;The critical rule: &lt;strong&gt;never use prompt-based hooks for safety boundaries.&lt;/strong&gt; Prompt hooks involve LLM judgment, and LLMs can be wrong. Safety boundaries need deterministic command hooks.&lt;/p&gt;




&lt;h2&gt;
  
  
  When should you use CLAUDE.md vs a hook vs both?
&lt;/h2&gt;

&lt;p&gt;Use CLAUDE.md for conventions the agent should follow. Use hooks for rules the agent must never break. Use both when you want the agent to understand WHY while the hook enforces WHAT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this a HARD constraint (must NEVER be violated)?
├── YES → Can you test it with a string/path/regex check?
│         ├── YES → Command hook (PreToolUse)
│         └── NO  → Does it need codebase context?
│                   ├── YES → Agent hook
│                   └── NO  → Prompt hook or HTTP hook
└── NO  → Is it a preference or convention?
              ├── YES → CLAUDE.md (~70-90% compliance)
              └── NO  → Is it a repeatable workflow?
                        ├── YES → Skill or .claude/commands/
                        └── NO  → You probably don't need it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When should you use both? When the constraint is structural (hook enforces it) but the agent also benefits from understanding the reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hook&lt;/strong&gt;: PreToolUse blocks &lt;code&gt;git push --force&lt;/code&gt; to main&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLAUDE.md&lt;/strong&gt;: "We use &lt;code&gt;--force-with-lease&lt;/code&gt; instead of &lt;code&gt;--force&lt;/code&gt; because a force push overwrote a teammate's commits in March 2026"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hook prevents the bad action. The CLAUDE.md helps the agent choose the right alternative.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which hook events should you implement first?
&lt;/h2&gt;

&lt;p&gt;Start with 3 events in this order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Handler&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;td&gt;PreToolUse&lt;/td&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;Block dangerous actions&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2nd&lt;/td&gt;
&lt;td&gt;PostToolUse&lt;/td&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;Auto-format, log actions&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3rd&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;agent&lt;/td&gt;
&lt;td&gt;Verify work before done&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4th&lt;/td&gt;
&lt;td&gt;SessionStart&lt;/td&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;Load env vars, context&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5th&lt;/td&gt;
&lt;td&gt;SubagentStop&lt;/td&gt;
&lt;td&gt;prompt&lt;/td&gt;
&lt;td&gt;Validate subagent output&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6th&lt;/td&gt;
&lt;td&gt;PermissionRequest&lt;/td&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;Auto-approve safe patterns&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7th&lt;/td&gt;
&lt;td&gt;PreCompact&lt;/td&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;Preserve context on compact&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your first hook — a PreToolUse command hook that blocks force pushes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/block-force-push.sh&lt;/span&gt;
&lt;span class="c"&gt;# Blocks git push --force and -f to main/master/production&lt;/span&gt;

&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // empty'&lt;/span&gt; &amp;lt; /dev/stdin&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'git push.*(--force|-f)'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'(main|master|production)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BLOCKED: force push to protected branch"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/block-force-push.sh"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How do you handle multiple hooks on the same event?
&lt;/h2&gt;

&lt;p&gt;Hooks on the same event run in definition order. For PreToolUse, the strictest decision wins: deny beats defer, defer beats ask, ask beats allow. If any hook denies, the action is blocked regardless of what other hooks return.&lt;/p&gt;

&lt;p&gt;Chain hooks from fastest to slowest to minimize latency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/block-force-push.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/validate-paths.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/log-action.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decision precedence hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deny   → Action blocked. Feedback sent to model.
defer  → Action paused (headless mode). External UI resumes.
ask    → User prompted for confirmation.
allow  → Action proceeds. Skips built-in permission check.
(none) → Default behavior. Built-in permission check runs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What are the most common hook mistakes?
&lt;/h2&gt;

&lt;p&gt;Three mistakes account for most "my hook doesn't work" reports:&lt;/p&gt;

&lt;h3&gt;
  
  
  Exit code cheat sheet
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Model Sees Feedback?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Success (parse JSON from stdout)&lt;/td&gt;
&lt;td&gt;Yes, if JSON provided&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Block action (stderr becomes feedback)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any other&lt;/td&gt;
&lt;td&gt;Silent error (logged in verbose only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exit 1 vs exit 2 distinction is the #1 gotcha. Exit 1 means "my hook crashed." Claude Code logs it quietly and continues. Exit 2 means "I'm deliberately blocking this action."&lt;/p&gt;

&lt;h3&gt;
  
  
  Debug workflow
&lt;/h3&gt;

&lt;p&gt;Test any hook manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Bash","tool_input":{"command":"git push --force main"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | bash .claude/hooks/block-force-push.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Exit code: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the hook doesn't run at all, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path correct?&lt;/strong&gt; Command path is relative to project root, not the hooks directory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matcher correct?&lt;/strong&gt; &lt;code&gt;"matcher": "Bash"&lt;/code&gt; matches the tool name, not the command content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings level?&lt;/strong&gt; Project &lt;code&gt;.claude/settings.json&lt;/code&gt; overrides user &lt;code&gt;~/.claude/settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File executable?&lt;/strong&gt; Run &lt;code&gt;chmod +x .claude/hooks/your-hook.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON valid?&lt;/strong&gt; A syntax error in settings.json silently disables all hooks&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are the 4 Claude Code hook handler types?
&lt;/h3&gt;

&lt;p&gt;Command (shell scripts, &amp;lt;5ms, deterministic), prompt (LLM judgment, 300-2000ms), agent (multi-turn verification with codebase access, 2-10s), and http (webhooks, 50-500ms). Use command hooks for guardrails and formatting. Use prompt or agent hooks for nuanced decisions that require reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use CLAUDE.md or a hook for security rules?
&lt;/h3&gt;

&lt;p&gt;Hooks. CLAUDE.md instructions achieve 70-90% compliance because they compete with 200K tokens of context. A PreToolUse command hook achieves 100% compliance because it runs outside the LLM's reasoning chain. Use CLAUDE.md to explain WHY. Use hooks to enforce WHAT.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between PreToolUse and PostToolUse hooks?
&lt;/h3&gt;

&lt;p&gt;PreToolUse runs BEFORE a tool executes and can block it (exit code 2) or modify its input. PostToolUse runs AFTER execution and cannot undo the action, but it can auto-format code, log what happened, or inject feedback. PreToolUse for prevention, PostToolUse for reaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Claude Code hooks run in headless mode?
&lt;/h3&gt;

&lt;p&gt;Yes. All hook types work in headless mode (&lt;code&gt;claude -p&lt;/code&gt;). PreToolUse hooks can return &lt;code&gt;permissionDecision: "defer"&lt;/code&gt; to pause execution for external UI collection. This makes hooks fully compatible with CI/CD pipelines and SDK-based workflows.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it now:&lt;/strong&gt; Copy the force-push blocker script into &lt;code&gt;.claude/hooks/block-force-push.sh&lt;/code&gt;, register it in &lt;code&gt;.claude/settings.json&lt;/code&gt;, make it executable with &lt;code&gt;chmod +x&lt;/code&gt;, and test it with the debug command above. Verify exit code 2. You now have one production-ready guardrail.&lt;/p&gt;

&lt;p&gt;Which hook event would you implement first? Drop it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shipwithai.io/blog/claude-code-hook-decision-guide/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-hook-decision-guide" rel="noopener noreferrer"&gt;ShipWithAI&lt;/a&gt;. I write about Claude Code workflows, AI-assisted development, and shipping software faster with structured AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>4 Lines in ~/.npmrc That Block 80% of npm Supply Chain Attacks</title>
      <dc:creator>ShipWithAI</dc:creator>
      <pubDate>Mon, 04 May 2026 01:00:00 +0000</pubDate>
      <link>https://dev.to/shipwithaiio/4-lines-in-npmrc-that-block-80-of-npm-supply-chain-attacks-1acp</link>
      <guid>https://dev.to/shipwithaiio/4-lines-in-npmrc-that-block-80-of-npm-supply-chain-attacks-1acp</guid>
      <description>&lt;p&gt;Four lines in &lt;code&gt;~/.npmrc&lt;/code&gt; block the most common npm supply chain attacks before they execute. Setup takes 30 seconds. This is the bare-minimum defense for anyone letting Claude Code or Cursor run &lt;code&gt;npm install&lt;/code&gt; on their machine.&lt;/p&gt;




&lt;p&gt;These four lines are on my laptop right now. I added them the morning the axios news broke and forgot about them. Since then, every &lt;code&gt;npm install&lt;/code&gt; Claude Code has run on my machine, across five side projects, has skipped lifecycle scripts by default. Zero breakage. Zero effort.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.npmrc
&lt;/span&gt;&lt;span class="py"&gt;ignore-scripts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;save-exact&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;audit-level&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;moderate&lt;/span&gt;
&lt;span class="py"&gt;fund&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 2025, attackers published &lt;strong&gt;454,648 malicious npm packages&lt;/strong&gt; — roughly half a million in a single year (&lt;a href="https://www.sonatype.com/blog/open-source-malware-index-q4-2025-automation-overwhelms-ecosystems" rel="noopener noreferrer"&gt;Sonatype Open Source Malware Index, 2026&lt;/a&gt;). The four lines above block the most common payload mechanism (lifecycle scripts) for every project on your laptop, including whatever Claude Code ran at 2am last night.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why is your default npm setup unsafe in 2026?
&lt;/h2&gt;

&lt;p&gt;npm ships with lifecycle scripts enabled by default. That means any package, direct or transitive, can execute arbitrary code on your machine during &lt;code&gt;npm install&lt;/code&gt; — before you ever type &lt;code&gt;require()&lt;/code&gt;. Over 99% of all open source malware now targets npm.&lt;/p&gt;

&lt;p&gt;Here's the same attack pattern, compressed across eight years:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;Payload vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;event-stream (Bitcoin wallet stealer, 2M/wk)&lt;/td&gt;
&lt;td&gt;postinstall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025 Sep&lt;/td&gt;
&lt;td&gt;Shai-Hulud worm, 18 packages, 2.6B/wk downloads&lt;/td&gt;
&lt;td&gt;postinstall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026 Mar&lt;/td&gt;
&lt;td&gt;
&lt;a href="mailto:axios@1.14.1"&gt;axios@1.14.1&lt;/a&gt; RAT, 100M/wk downloads&lt;/td&gt;
&lt;td&gt;postinstall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three incidents across eight years. Same mechanism every time. npm's official response each time is to unpublish the package and write a blog post. No structural change to how &lt;code&gt;postinstall&lt;/code&gt; works.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: 84% of developers use AI coding tools, and 41% of code written in 2025 was AI-generated or AI-assisted. AI agents install packages at machine speed, with approval fatigue doing the rest. The human review step that used to catch weird dependencies has already been deleted from most workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What each line does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ignore-scripts=true&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Disables &lt;code&gt;preinstall&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, and &lt;code&gt;postinstall&lt;/code&gt; lifecycle scripts for every &lt;code&gt;npm install&lt;/code&gt;. The OWASP NPM Security Cheat Sheet calls this the single most effective mitigation against malicious or compromised packages. The &lt;a href="mailto:axios@1.14.1"&gt;axios@1.14.1&lt;/a&gt; RAT, the Shai-Hulud worm, event-stream's Bitcoin stealer — all needed this mechanism to execute. Turn it off globally and the default delivery vehicle is gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;save-exact=true&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Pins exact versions in &lt;code&gt;package.json&lt;/code&gt; whenever you add a package. Without it, &lt;code&gt;npm install axios&lt;/code&gt; writes &lt;code&gt;"axios": "^1.14.0"&lt;/code&gt;, a caret range that resolves to &lt;code&gt;1.14.1&lt;/code&gt; on the next clean install. With &lt;code&gt;save-exact=true&lt;/code&gt;, the same command writes &lt;code&gt;"axios": "1.14.0"&lt;/code&gt;. A hijacked patch release cannot silently promote itself into your lockfile.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;audit-level=moderate&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Raises &lt;code&gt;npm install&lt;/code&gt; exit code when known CVEs of moderate or higher severity are present. Default behavior is warn-only. This flag makes audit block instead — which means CI or Claude Code sessions fail loud rather than scrolling past.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;fund=false&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Removes the "N packages are looking for funding" message from every install. Cosmetic, but it matters. When your install output is 80% funding notices, the warnings that actually matter (audit, deprecation, peer dependency conflicts) get buried. Signal hygiene is a security layer.&lt;/p&gt;

&lt;p&gt;Verify your config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm config get ignore-scripts save-exact audit-level fund
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;moderate&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does this work for most npm attacks?
&lt;/h2&gt;

&lt;p&gt;The dominant payload pattern in 2025 and 2026 npm attacks is a lifecycle script that runs during install. Disabling those scripts breaks the default delivery vehicle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack vector&lt;/th&gt;
&lt;th&gt;Real example&lt;/th&gt;
&lt;th&gt;Line that blocks it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;postinstall RAT&lt;/td&gt;
&lt;td&gt;
&lt;a href="mailto:axios@1.14.1"&gt;axios@1.14.1&lt;/a&gt; (2026)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ignore-scripts=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent minor/patch hijack&lt;/td&gt;
&lt;td&gt;Maintainer account takeover&lt;/td&gt;
&lt;td&gt;&lt;code&gt;save-exact=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Known CVE buried as warning&lt;/td&gt;
&lt;td&gt;Any reported advisory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;audit-level=moderate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning fatigue hiding alerts&lt;/td&gt;
&lt;td&gt;Every install, all day&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fund=false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What does this NOT protect against?
&lt;/h2&gt;

&lt;p&gt;Honest boundaries. This config blocks the most common vector, not every vector:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Malicious code in the package's main module.&lt;/strong&gt; Anything that runs on &lt;code&gt;require()&lt;/code&gt; or &lt;code&gt;import&lt;/code&gt; is unaffected by &lt;code&gt;ignore-scripts&lt;/code&gt;. If the package is actively imported by your code, the payload runs at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Toolchain exploits.&lt;/strong&gt; &lt;code&gt;--ignore-scripts&lt;/code&gt; stops npm lifecycle hooks, but git still runs during install, and external binaries still execute if the install process invokes them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typosquatting and slopsquatting.&lt;/strong&gt; AI assistants sometimes hallucinate package names that attackers have preemptively registered. OWASP flags this as the fastest-growing npm attack class in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Packages already in &lt;code&gt;node_modules&lt;/code&gt;.&lt;/strong&gt; The four lines only protect future installs. Clean rebuild recommended: &lt;code&gt;rm -rf node_modules package-lock.json &amp;amp;&amp;amp; npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Roughly 20% of recent high-impact npm malware executes outside lifecycle scripts through runtime &lt;code&gt;require()&lt;/code&gt; or compromised main modules. Treat &lt;code&gt;.npmrc&lt;/code&gt; as necessary-but-not-sufficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  What breaks when you set &lt;code&gt;ignore-scripts=true&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;A small set of packages genuinely need lifecycle scripts to compile native binaries or download platform assets. The usual suspects: &lt;code&gt;bcrypt&lt;/code&gt;, &lt;code&gt;node-sass&lt;/code&gt;, &lt;code&gt;sharp&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, &lt;code&gt;puppeteer&lt;/code&gt;, and &lt;code&gt;canvas&lt;/code&gt;. You will notice immediately because they fail loud, not silent.&lt;/p&gt;

&lt;p&gt;Fix per-package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install normally, then rebuild the one package that needs it&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;sharp
npm rebuild sharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For projects with multiple native-compile dependencies, use an allow-list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @lavamoat/allow-scripts
npx allow-scripts auto
npx allow-scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Why it needs scripts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bcrypt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native C++ compilation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sharp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Binary download + native bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-sass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LibSass native build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;esbuild&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Platform binary download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;puppeteer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chromium download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;canvas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cairo/Pango native bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ninety percent of projects never hit any of these. The ones that do fail on the first CI run after the config change, and you fix them once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrading to hook-based defense
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.npmrc&lt;/code&gt; is the user-scoped floor. The next layer is process-level enforcement: intercepting every &lt;code&gt;npm install&lt;/code&gt; Claude Code tries to run, auditing it before the command executes, and blocking the call if it's missing &lt;code&gt;--ignore-scripts&lt;/code&gt; or pointing at a new unreviewed dependency.&lt;/p&gt;

&lt;p&gt;With 41% of all code now AI-generated or AI-assisted, the agent — not the human — is the primary &lt;code&gt;npm install&lt;/code&gt; trigger. That's a &lt;code&gt;PreToolUse&lt;/code&gt; hook in &lt;code&gt;.claude/settings.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://shipwithai.io/blog/claude-code-npm-supply-chain-hooks/" rel="noopener noreferrer"&gt;hook post&lt;/a&gt; covers the three-layer setup: PreToolUse audit, PostToolUse lockfile diff, and CLAUDE.md enforcement rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Will &lt;code&gt;ignore-scripts=true&lt;/code&gt; break my builds?
&lt;/h3&gt;

&lt;p&gt;Usually no for pure-JavaScript dependencies, which is 90%+ of a typical React or Node project. Yes for native-compile packages like &lt;code&gt;bcrypt&lt;/code&gt;, &lt;code&gt;sharp&lt;/code&gt;, and &lt;code&gt;esbuild&lt;/code&gt;. Fix is &lt;code&gt;npm rebuild &amp;lt;pkg&amp;gt;&lt;/code&gt; per package or &lt;code&gt;@lavamoat/allow-scripts&lt;/code&gt; for a team-wide allow-list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I commit &lt;code&gt;.npmrc&lt;/code&gt; to my repo?
&lt;/h3&gt;

&lt;p&gt;Personal config goes in &lt;code&gt;~/.npmrc&lt;/code&gt; (never committed, user defaults). Project-level &lt;code&gt;.npmrc&lt;/code&gt; at the repo root can be committed as long as it contains no secrets. Registry auth tokens belong only in &lt;code&gt;~/.npmrc&lt;/code&gt;, never in the repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this work for pnpm and yarn?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.npmrc&lt;/code&gt; is shared. pnpm reads &lt;code&gt;ignore-scripts=true&lt;/code&gt; natively. Yarn classic also reads &lt;code&gt;.npmrc&lt;/code&gt;. Yarn Berry uses &lt;code&gt;.yarnrc.yml&lt;/code&gt; instead, and the equivalent setting is &lt;code&gt;enableScripts: false&lt;/code&gt;. Bun also honors &lt;code&gt;.npmrc&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;npm audit&lt;/code&gt; still useful if I set &lt;code&gt;audit-level=moderate&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Yes, and it becomes more useful. The flag changes audit from warn-mode to block-mode on CVEs at moderate severity or higher. Audit still only catches &lt;em&gt;published&lt;/em&gt; CVEs. For zero-days, you need the hook layer from the &lt;a href="https://shipwithai.io/blog/claude-code-npm-supply-chain-hooks/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-npm-supply-chain-hooks" rel="noopener noreferrer"&gt;hooks post&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it now:&lt;/strong&gt; Open a terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"ignore-scripts=true&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;save-exact=true&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;audit-level=moderate&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;fund=false"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.npmrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify with &lt;code&gt;npm config get ignore-scripts save-exact audit-level fund&lt;/code&gt;. Total time: under 30 seconds.&lt;/p&gt;

&lt;p&gt;Ready for the process-level defense? Read → &lt;a href="https://shipwithai.io/blog/claude-code-npm-supply-chain-hooks/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-claude-code-npm-supply-chain-hooks" rel="noopener noreferrer"&gt;Stop npm Supply Chain Attacks with Claude Code Hooks&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shipwithai.io/blog/npm-install-security-30-seconds/?utm_source=copy&amp;amp;utm_medium=devto&amp;amp;utm_campaign=blog-npm-install-security-30-seconds" rel="noopener noreferrer"&gt;ShipWithAI&lt;/a&gt;. I write about Claude Code workflows, AI-assisted development, and shipping software faster with structured AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>security</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
