<?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>Forem: Tom Howard</title>
    <description>The latest articles on Forem by Tom Howard (@tompahoward).</description>
    <link>https://forem.com/tompahoward</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3813872%2Fdb277518-9688-4db4-ba1f-eb73c2060084.jpeg</url>
      <title>Forem: Tom Howard</title>
      <link>https://forem.com/tompahoward</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tompahoward"/>
    <language>en</language>
    <item>
      <title>Stop your AI agent from ignoring your architecture</title>
      <dc:creator>Tom Howard</dc:creator>
      <pubDate>Fri, 20 Mar 2026 12:38:44 +0000</pubDate>
      <link>https://forem.com/tompahoward/stop-your-ai-agent-from-ignoring-your-architecture-3gc5</link>
      <guid>https://forem.com/tompahoward/stop-your-ai-agent-from-ignoring-your-architecture-3gc5</guid>
      <description>&lt;p&gt;An AI agent makes architectural decisions constantly. Add a dependency, change a build script, restructure a config file. Each choice is reasonable in isolation. None of them get written down.&lt;/p&gt;

&lt;p&gt;This is the knowledge management version of technical debt. Six months later, someone asks why the project uses rehype-highlight instead of Shiki. The answer is in a conversation that no longer exists. The decision was sound. The reasoning is gone.&lt;/p&gt;

&lt;p&gt;A hook-based gate can close this gap. This implementation uses &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;Claude Code hooks&lt;/a&gt; (&lt;a href="https://github.com/windyroad/windyroad" rel="noopener noreferrer"&gt;source code&lt;/a&gt;), but the pattern (detect, gate, review, unlock, reset) applies to any agent system that exposes lifecycle events.&lt;/p&gt;

&lt;p&gt;Claude Code hooks are shell scripts that run at specific points in the agent's lifecycle. &lt;code&gt;UserPromptSubmit&lt;/code&gt; fires when the user sends a message. &lt;code&gt;PreToolUse&lt;/code&gt; fires before the agent calls a tool (Edit, Write, Bash). &lt;code&gt;PostToolUse&lt;/code&gt; fires after a tool call completes. &lt;code&gt;Stop&lt;/code&gt; fires when the agent finishes its turn. Each hook receives JSON on stdin describing the event, and can inject context, allow the action, or deny it.&lt;/p&gt;

&lt;p&gt;The gate intercepts edits to project files and requires an architecture review before the edit proceeds. The reviewer checks proposed changes against existing decision records in &lt;code&gt;docs/decisions/&lt;/code&gt; and flags when a new decision should be documented.&lt;/p&gt;

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

&lt;p&gt;Architecture Decision Records solve a known problem: decisions made verbally or in chat disappear. The &lt;a href="https://adr.github.io/madr/" rel="noopener noreferrer"&gt;MADR format&lt;/a&gt; (Markdown Any Decision Records) gives them structure. Context, options considered, rationale, consequences, reassessment criteria.&lt;/p&gt;

&lt;p&gt;The format is not the hard part. The hard part is remembering to write them. An AI agent adding a dependency to &lt;code&gt;package.json&lt;/code&gt; will not stop to ask itself whether this choice deserves a decision record. It will install the package and move on.&lt;/p&gt;

&lt;p&gt;The same problem exists with compliance. If decision 001 says "use rehype-highlight for syntax highlighting," nothing stops the agent from adding &lt;code&gt;@shikijs/rehype&lt;/code&gt; to &lt;code&gt;package.json&lt;/code&gt; in a later session. The decision exists. The agent doesn't check it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five hooks, one gate
&lt;/h2&gt;

&lt;p&gt;Five hooks enforce the gate. Four follow a cycle: detect that the project has an architect agent, block edits until the architect reviews them, unlock the block when the review passes, reset the block when the turn ends. A fifth hook blocks exiting plan mode without a review. This is a variation of the pattern used for &lt;a href="https://windyroad.com.au/blog/enforcing-voice-and-tone-with-claude-code-hooks" rel="noopener noreferrer"&gt;voice and tone enforcement&lt;/a&gt;, with additional hardening.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Hook event&lt;/th&gt;
&lt;th&gt;Script&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Detect&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserPromptSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;architect-detect.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inject review instructions on every prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PreToolUse&lt;/code&gt; (Edit\&lt;/td&gt;
&lt;td&gt;Write)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;architect-enforce-edit.sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan gate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PreToolUse&lt;/code&gt; (ExitPlanMode)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;architect-plan-enforce.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block plan exit without review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unlock&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PostToolUse&lt;/code&gt; (Agent)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;architect-mark-reviewed.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create marker when architect passes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;architect-reset-marker.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove marker so next turn starts locked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Farchitect-five-hooks.png" 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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Farchitect-five-hooks.png" alt="Flow diagram showing the five-hook architect gate" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The gate
&lt;/h3&gt;

&lt;p&gt;The code samples below are excerpts. The complete hook scripts are each under 40 lines. Common functions (portable mtime, hash computation, marker validation) are extracted into &lt;code&gt;lib/architect-gate.sh&lt;/code&gt;, which the other scripts source.&lt;/p&gt;

&lt;p&gt;The gate is fail-closed. It parses the hook input with &lt;code&gt;jq&lt;/code&gt;, and if parsing fails, the edit is blocked. From &lt;code&gt;architect-enforce-edit.sh&lt;/code&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="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;FILE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&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;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&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;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.session_id // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&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="c"&gt;# Fail-closed: block on parse failure&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{ "hookSpecificOutput": { "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "BLOCKED: Could not parse hook input." } }
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file is not excluded (CSS, images, lockfiles, fonts, memory files) and no valid session marker exists, the edit is denied. The same check runs on &lt;code&gt;ExitPlanMode&lt;/code&gt;, sharing the marker.&lt;/p&gt;

&lt;h3&gt;
  
  
  The unlock
&lt;/h3&gt;

&lt;p&gt;The unlock only fires after the architect agent returns. It reads a verdict file that the architect writes during its review. From &lt;code&gt;architect-mark-reviewed.sh&lt;/code&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="nv"&gt;VERDICT_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/architect-verdict"&lt;/span&gt;
&lt;span class="nv"&gt;VERDICT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERDICT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&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="nv"&gt;VERDICT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERDICT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERDICT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi

case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERDICT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;PASS&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/architect-reviewed-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  FAIL&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="c"&gt;# Do NOT create marker&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# No verdict file: allow to prevent permanent lockout&lt;/span&gt;
    &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/architect-reviewed-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the verdict is FAIL, no marker is created and edits stay blocked until the issues are resolved and the architect is re-run. The missing-verdict fallback defaults to PASS to prevent permanent lockout if the agent errors out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marker validity
&lt;/h2&gt;

&lt;p&gt;A marker file in &lt;code&gt;/tmp&lt;/code&gt; is not enough. Three checks run before the gate allows an edit through.&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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Farchitect-marker-validity.png" 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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Farchitect-marker-validity.png" alt="Marker validity flow diagram" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL.&lt;/strong&gt; The marker has a configurable time-to-live, defaulting to 600 seconds. If the marker is older than this, it is removed and the gate blocks. The TTL is configurable via the &lt;code&gt;ARCHITECT_TTL&lt;/code&gt; environment variable. From &lt;code&gt;lib/architect-gate.sh&lt;/code&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="nv"&gt;TTL_SECONDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCHITECT_TTL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;600&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;NOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;MARKER_TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;_mtime &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;AGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; NOW &lt;span class="o"&gt;-&lt;/span&gt; MARKER_TIME &lt;span class="k"&gt;))&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;$AGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TTL_SECONDS&lt;/span&gt;&lt;span class="s2"&gt;"&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="c"&gt;# Still valid, proceed to drift check&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sliding window.&lt;/strong&gt; Each successful gate pass refreshes the marker timestamp. A long editing session is not interrupted as long as edits are less than 10 minutes apart. The TTL catches abandoned markers, not active work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drift detection.&lt;/strong&gt; When the unlock hook creates a marker, it also stores a content hash of all files in &lt;code&gt;docs/decisions/&lt;/code&gt;. From &lt;code&gt;architect-mark-reviewed.sh&lt;/code&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="nv"&gt;HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find docs/decisions &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'README.md'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-print0&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | _hashcmd | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&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;$HASH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/architect-reviewed-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.hash"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before allowing an edit, the gate recomputes the hash and compares it to the stored value. If a decision file changed since the review, the marker is invalidated and a re-review is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reviewer
&lt;/h2&gt;

&lt;p&gt;The architect agent is defined in &lt;code&gt;.claude/agents/architect.md&lt;/code&gt;. It has read-only access (Read, Glob, Grep) plus Bash for writing the verdict file. It cannot edit project files.&lt;/p&gt;

&lt;p&gt;It checks five things, in order of importance. The first three affect the PASS/FAIL verdict. The last two are advisory.&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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Farchitect-reviewer-checks.png" 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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Farchitect-reviewer-checks.png" alt="Reviewer checks diagram" width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing decision compliance.&lt;/strong&gt; For each decision in &lt;code&gt;docs/decisions/&lt;/code&gt;, does the proposed change conflict with the decision's outcome? Does it violate documented constraints or consequences?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirmation criteria.&lt;/strong&gt; Many decisions include a Confirmation section describing how to verify compliance (e.g. "Client JS does not contain hardcoded API URLs beyond the entry point"). The agent checks proposed code against these criteria and flags violations as &lt;code&gt;[Confirmation Violation]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New decision detection.&lt;/strong&gt; Does the change represent an undocumented architectural choice? The agent is told to be pragmatic. A version bump to an existing dependency is reversible and local: no flag. Adding a new ORM, switching from REST to GraphQL, or introducing a new CI workflow affects how the team works and how code flows to production: flag it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision quality.&lt;/strong&gt; When a change includes a new decision file, does it follow MADR 4.0 format? Required frontmatter, at least two considered options, reassessment criteria.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision staleness (advisory).&lt;/strong&gt; If an &lt;code&gt;accepted&lt;/code&gt; decision is older than 6 months, the agent flags &lt;code&gt;[Stale Decision]&lt;/code&gt;. If a &lt;code&gt;reassessment-date&lt;/code&gt; has passed, &lt;code&gt;[Reassessment Overdue]&lt;/code&gt;. These do not affect the PASS/FAIL verdict.&lt;/p&gt;

&lt;p&gt;A typical review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Architecture Review: PASS&lt;/strong&gt;&lt;br&gt;
No conflicts with existing decisions. No new architectural decision required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Architecture Review: ISSUES FOUND&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;[Undocumented Decision]&lt;/strong&gt; - File: &lt;code&gt;package.json&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issue&lt;/strong&gt;: Adding &lt;code&gt;@shikijs/rehype&lt;/code&gt; as a dependency. Decision 001 chose &lt;code&gt;rehype-highlight&lt;/code&gt; over Shiki.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing Decision&lt;/strong&gt;: 001-use-rehype-highlight-for-syntax-highlighting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: This conflicts with an accepted decision. Either update the decision or remove the dependency.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What gets gated
&lt;/h2&gt;

&lt;p&gt;The gate excludes files by extension. From &lt;code&gt;architect-enforce-edit.sh&lt;/code&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="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;.css|&lt;span class="k"&gt;*&lt;/span&gt;.scss|&lt;span class="k"&gt;*&lt;/span&gt;.sass|&lt;span class="k"&gt;*&lt;/span&gt;.less&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Styles&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;.png|&lt;span class="k"&gt;*&lt;/span&gt;.jpg|&lt;span class="k"&gt;*&lt;/span&gt;.jpeg|&lt;span class="k"&gt;*&lt;/span&gt;.gif|&lt;span class="k"&gt;*&lt;/span&gt;.svg|&lt;span class="k"&gt;*&lt;/span&gt;.ico&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Images&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;.woff|&lt;span class="k"&gt;*&lt;/span&gt;.woff2|&lt;span class="k"&gt;*&lt;/span&gt;.ttf|&lt;span class="k"&gt;*&lt;/span&gt;.eot&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Fonts&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;package-lock.json|&lt;span class="k"&gt;*&lt;/span&gt;yarn.lock&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Lockfiles&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;.map&lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Sourcemaps&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;.changeset/&lt;span class="k"&gt;*&lt;/span&gt;.md&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Changesets&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;/MEMORY.md|&lt;span class="k"&gt;*&lt;/span&gt;/.claude/projects/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Memory files&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;/.claude/plans/&lt;span class="k"&gt;*&lt;/span&gt;.md&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;  &lt;span class="c"&gt;# Plan files&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else goes through the gate. Adjust the list for your project: if you want to gate only infrastructure files, narrow the exclusions. The architect agent is told to be pragmatic: a refactored function or a bug fix gets a quick PASS. A new API endpoint that skips an established pattern gets flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisions as living documents
&lt;/h2&gt;

&lt;p&gt;Decisions follow a lifecycle. They start as &lt;code&gt;proposed&lt;/code&gt;, move to &lt;code&gt;accepted&lt;/code&gt; after production validation, and eventually get &lt;code&gt;deprecated&lt;/code&gt; or &lt;code&gt;superseded&lt;/code&gt;. The status lives in the filename: &lt;code&gt;001-use-rehype-highlight.proposed.md&lt;/code&gt; becomes &lt;code&gt;001-use-rehype-highlight.accepted.md&lt;/code&gt; after the site ships with rehype-highlight and nothing breaks.&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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Fdecision-lifecycle.png" 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%2Fwindyroad.com.au%2Fimg%2Fsocial%2Fdecision-lifecycle.png" alt="Decision lifecycle diagram" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this project, that decision started as proposed when the agent flagged &lt;code&gt;rehype-highlight&lt;/code&gt; as an undocumented dependency. The MADR record captured why Shiki was rejected (bundle size, build complexity) and when to revisit (if rehype-highlight drops maintained status). Three deploys later, the decision moved to accepted. Now when the agent sees a new syntax highlighting dependency in &lt;code&gt;package.json&lt;/code&gt;, it has context: not just what was chosen, but why, and under what conditions to reconsider.&lt;/p&gt;

&lt;p&gt;Without automation, promotion does not happen. Decisions stay &lt;code&gt;proposed&lt;/code&gt; indefinitely because nothing triggers the rename after a successful deploy. A post-release hook closes this gap. It runs after each deploy as a drop-in script in &lt;code&gt;scripts/post-release.d/&lt;/code&gt;, receiving the list of changed files on stdin and the release date as an environment variable.&lt;/p&gt;

&lt;p&gt;The hook works in two passes. From &lt;code&gt;stamp-and-promote-decisions.sh&lt;/code&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;# Pass 1: Stamp first-released on proposed decisions included in this release&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DECISIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.proposed.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;has_field &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"first-released"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    continue&lt;/span&gt;  &lt;span class="c"&gt;# Already stamped&lt;/span&gt;
  &lt;span class="k"&gt;fi
  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;$FILE_LIST&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;-qF&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&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;sed&lt;/span&gt; &lt;span class="s2"&gt;"s/^status: *&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;*proposed&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;*/status: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;proposed&lt;/span&gt;&lt;span class="se"&gt;\"\n&lt;/span&gt;&lt;span class="s2"&gt;first-released: &lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_DATE&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# Pass 2: Promote decisions past the 14-day threshold&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DECISIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.proposed.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;FIRST_RELEASED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^first-released:"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;AGE_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;NOW_EPOCH &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;date_to_epoch &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIRST_RELEASED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&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;$AGE_DAYS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMOTION_DAYS&lt;/span&gt;&lt;span class="s2"&gt;"&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;sed&lt;/span&gt; &lt;span class="s2"&gt;"s/^status: *&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;*proposed&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;*/status: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;accepted&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    git &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&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;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\.proposed\.md$/.accepted.md/'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass 1 stamps a &lt;code&gt;first-released&lt;/code&gt; date on each proposed decision that shipped in the release. Pass 2 checks all stamped decisions and promotes any that have been in production longer than 14 days (configurable via &lt;code&gt;DECISION_PROMOTION_DAYS&lt;/code&gt;). The promotion renames the file from &lt;code&gt;.proposed.md&lt;/code&gt; to &lt;code&gt;.accepted.md&lt;/code&gt;, updates the frontmatter status, and adds an &lt;code&gt;accepted-date&lt;/code&gt; field. Any changes are committed and pushed as part of the release.&lt;/p&gt;

&lt;p&gt;The 14-day grace period exists so that decisions can be reverted if they cause production issues. A decision that ships on Monday and breaks something on Wednesday can be rolled back before it gets promoted. The architect agent enforces compliance against both &lt;code&gt;proposed&lt;/code&gt; and &lt;code&gt;accepted&lt;/code&gt; decisions but ignores &lt;code&gt;superseded&lt;/code&gt; ones. A rejected decision prevents re-proposing the same approach without new evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;The architect agent call adds 10-20 seconds per turn that touches project files. The sliding TTL means this cost is paid once per session, not once per edit, as long as edits are less than 10 minutes apart.&lt;/p&gt;

&lt;p&gt;False negatives are more dangerous than false positives. The agent might miss a decision-worthy change because the pragmatism criteria were too generous, or because the change didn't match any detection patterns. There's no exhaustive list of what constitutes an architectural decision. The agent approximates.&lt;/p&gt;

&lt;p&gt;The system now runs in two repos. A second project (&lt;a href="https://github.com/windyroad/bbstats" rel="noopener noreferrer"&gt;bbstats&lt;/a&gt;) has 33 architecture decisions: 11 promoted from proposed to accepted via the release hook, 3 superseded, and 19 still proposed. The hooks and agent definition were copied to the second repo without changes. Every major feature in that project's changelog references an ADR. Decisions accumulate at the &lt;code&gt;proposed&lt;/code&gt; stage and batch-promote when a release crosses the 14-day threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict gating
&lt;/h2&gt;

&lt;p&gt;The verdict gating matters more than it looks. In an earlier version of this system (before the PASS/FAIL verdict file), the architect flagged issues but the gate unlocked regardless. The AI could proceed with edits while leaving the flagged issues unresolved.&lt;/p&gt;

&lt;p&gt;A real example: the AI was removing an unused API endpoint. The architect flagged that a smoke test depended on it and recommended updating the smoke test to check something that validates the health of the system. Without verdict gating, the AI proceeded with the rest of the task, left the API in place, left the smoke test unchanged, and moved on. The architect caught the problem. The AI chose the path of least resistance: do nothing about it.&lt;/p&gt;

&lt;p&gt;With verdict gating, the gate stays locked after ISSUES FOUND. The AI has two options: fix the smoke test and remove the API properly, or stop. No middle ground where you half-do the work and leave broken dependencies in place. The hook system cannot make the AI choose the right fix. But it can prevent the AI from ignoring the issue and continuing as if the review never happened.&lt;/p&gt;

&lt;p&gt;The system has known edge cases. Marker files live in &lt;code&gt;/tmp&lt;/code&gt;, which is world-writable and not shared across machines. Drift detection hashes file contents, not filenames, so renaming a decision file without changing its content won't trigger a re-review. Concurrent sessions share the verdict file, creating a small race window (the PostToolUse hook reads and deletes it immediately, so the window is the time between two architect agents finishing simultaneously).&lt;/p&gt;

&lt;p&gt;The gate blocks the AI, not you. The hooks constrain the agent's workflow. You control the hooks, the agent definition, and the decisions. If the architect flags something you disagree with, adjust the decision or the agent's instructions. The system is yours to tune.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting this for your project
&lt;/h2&gt;

&lt;p&gt;Start with the agent file. Drop &lt;code&gt;.claude/agents/architect.md&lt;/code&gt; into your repo. The embedded process works out of the box with an empty &lt;code&gt;docs/decisions/&lt;/code&gt; directory. The first time the agent flags an undocumented decision, create the directory and the first record.&lt;/p&gt;

&lt;p&gt;Wire the five hooks (detection, gate, plan gate, unlock, reset) into &lt;code&gt;.claude/settings.json&lt;/code&gt;. The full configuration, including matchers and hook scripts, is in the &lt;a href="https://github.com/windyroad/windyroad" rel="noopener noreferrer"&gt;source repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Adjust the scope. The exclusion list matches this project's structure. If you want to gate only infrastructure files instead of everything, modify the case statement to match only the file types you care about. The pattern is the same.&lt;/p&gt;

&lt;p&gt;Bootstrap your decisions. Once the hooks are wired, ask the AI to survey the codebase and document the existing architectural choices as decision records. The architect agent already knows the MADR 4.0 format and will create records for the technology choices, patterns, and conventions it finds. Review what it produces, fill in any context the agent missed (the "why" behind a choice is often not in the code), and add reassessment criteria. This gives you a populated &lt;code&gt;docs/decisions/&lt;/code&gt; directory in one session instead of building it incrementally over months.&lt;/p&gt;

&lt;p&gt;Wire the release hook. Drop &lt;code&gt;scripts/post-release.d/stamp-and-promote-decisions.sh&lt;/code&gt; into your repo and call it from your release script after deploy. It handles both normal runs (file list on stdin) and cold-start backfill (checks git history for first-release dates). The 14-day promotion threshold is configurable via &lt;code&gt;DECISION_PROMOTION_DAYS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The pattern works in any agent system that exposes lifecycle events. Cursor has Rules for AI that can inject context and constrain behavior. The key requirements are three: inject instructions before the agent acts, intercept file writes, and run a check after the agent finishes. The gate logic (marker files, TTL, drift detection) is plain shell and works anywhere.&lt;/p&gt;

&lt;p&gt;The full configuration is in the public repo at &lt;a href="https://github.com/windyroad/windyroad" rel="noopener noreferrer"&gt;github.com/windyroad/windyroad&lt;/a&gt;. The &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;Claude Code hooks documentation&lt;/a&gt; covers the full event model.&lt;/p&gt;

&lt;p&gt;The gate writes the decision down. The release hook tracks when it ships. Fourteen days later, the decision earns its place in the record. The reasoning that used to vanish in a chat thread now outlives the conversation that produced it.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Enforcing voice and tone with Claude Code hooks</title>
      <dc:creator>Tom Howard</dc:creator>
      <pubDate>Mon, 09 Mar 2026 21:02:09 +0000</pubDate>
      <link>https://forem.com/tompahoward/enforcing-voice-and-tone-with-claude-code-hooks-6db</link>
      <guid>https://forem.com/tompahoward/enforcing-voice-and-tone-with-claude-code-hooks-6db</guid>
      <description>&lt;p&gt;An AI agent writes fluent prose. It also writes whatever you ask for. Tell it to add a hero section and it will produce "We're passionate about leveraging cutting-edge solutions." Tell it to write an FAQ answer and it will open with "Great question!" Neither sounds like you.&lt;/p&gt;

&lt;p&gt;Voice consistency is a constraint, and constraints need enforcement. This system blocks the AI from editing copy until a voice-and-tone reviewer has checked the proposed changes against a written guide.&lt;/p&gt;

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

&lt;p&gt;A voice guide sitting in a markdown file is documentation. The AI reads it if you tell it to, ignores it if you don't, and drifts from it when generating long passages. A guide only describes the rules. Nothing enforces them.&lt;/p&gt;

&lt;p&gt;The failure mode is subtle. The AI won't produce obviously wrong copy. It will produce copy that's slightly off: a hedging phrase here, an "actually" there, a sentence that positions you against a competitor instead of stating what you do. Each deviation is small. Over time, the site sounds like it was written by committee.&lt;/p&gt;

&lt;p&gt;Here is what drift looks like in practice. An AI asked to write a CTA might produce:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We'd love to help you navigate the complexities of AI integration. Reach out today to learn how our team can support your journey.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The voice guide says: state what you do, let the reader decide. The reviewed version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I review your AI coding setup and tell you what's working and what isn't. Book a call if that's useful.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first version is fluent. It's also generic, hedging, and sounds like every other consulting site. The reviewer catches the pattern before it ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four hooks, one gate
&lt;/h2&gt;

&lt;p&gt;The pattern comes from &lt;a href="https://github.com/Community-Access/accessibility-agents" rel="noopener noreferrer"&gt;accessibility-agents&lt;/a&gt;, which uses the same hook architecture to enforce WCAG compliance on web UI code. I adapted it for voice and tone: same gate mechanism, different reviewer, different scope, plus a reset hook that tightens the review cycle from per-session to per-turn.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;Claude Code hooks&lt;/a&gt; are shell scripts that fire at defined points in the AI's workflow: before a prompt is processed, before a tool is called, after a tool completes, and when the AI finishes responding. They can inject context, block actions, or react to what just happened.&lt;/p&gt;

&lt;p&gt;The system uses four Claude Code hooks that work together. One injects the instruction. One blocks the edit. One unlocks the block. One resets the lock so the next prompt starts clean.&lt;/p&gt;

&lt;p&gt;Here is what a typical interaction looks like. The user asks for an edit. The gate blocks it. The AI delegates to the reviewer. The reviewer finds violations. The AI incorporates the fixes, and the edit goes through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Add a CTA to the pricing page

Claude: [attempts to edit src/app/pricing/page.tsx]

Hook: BLOCKED: Cannot edit copy file 'page.tsx' without voice &amp;amp; tone
      review. You MUST first delegate to voice-and-tone-lead.

Claude: [delegates to voice-and-tone-lead]

voice-and-tone-lead:
  VIOLATIONS FOUND
  1. [Direct voice principle] "We'd love to help you navigate the
     complexities" -- hedging. Fix: "I review your AI coding setup
     and tell you what's working and what isn't."

Claude: [incorporates fix, edits page.tsx successfully]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fvfduo9ctngz16u5uuskq.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fvfduo9ctngz16u5uuskq.png" alt="Flow diagram showing the four-hook gate pattern: a UserPromptSubmit hook detects VOICE-AND-TONE.md and injects context, a PreToolUse hook checks for a session marker and blocks edits to copy files if the marker is missing, a PostToolUse hook creates the session marker after voice-and-tone-lead completes, and a Stop hook removes the marker so the next turn requires a fresh review. Arrows show the cycle: detect, gate, unlock, reset."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Detection (UserPromptSubmit)
&lt;/h3&gt;

&lt;p&gt;Every prompt, a hook checks whether &lt;code&gt;VOICE-AND-TONE.md&lt;/code&gt; exists in the project root. If it does, the hook injects an instruction into the AI's context telling it to delegate to the voice-and-tone-lead agent before editing any copy file.&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;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"VOICE-AND-TONE.md"&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;HOOK_OUTPUT&lt;/span&gt;&lt;span class="sh"&gt;'
INSTRUCTION: MANDATORY VOICE &amp;amp; TONE CHECK. YOU MUST FOLLOW THIS.
DETECTED: VOICE-AND-TONE.md exists in this project.

This is a NON-OPTIONAL instruction. You MUST use the voice-and-tone-lead agent
before editing any user-facing copy in .tsx files under src/app/ or
src/components-next/, blog articles in src/articles/, or social posts in
src/social/. This is proactive. Do not wait for the user to ask.
&lt;/span&gt;&lt;span class="no"&gt;HOOK_OUTPUT
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instruction fires on every prompt, not just prompts that mention copy. The AI doesn't always know in advance whether a task will involve editing a copy file. A prompt like "fix the broken layout on the pricing page" might require changing text. The instruction ensures the reviewer is consulted regardless.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The gate (PreToolUse)
&lt;/h3&gt;

&lt;p&gt;The detection hook is context injection. Context can be ignored. The gate cannot.&lt;/p&gt;

&lt;p&gt;A PreToolUse hook fires before every Edit or Write call. It checks whether the target file is a copy-bearing file (&lt;code&gt;.tsx&lt;/code&gt; in &lt;code&gt;src/app/&lt;/code&gt; or &lt;code&gt;src/components-next/&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt; in &lt;code&gt;src/articles/&lt;/code&gt; or &lt;code&gt;src/social/&lt;/code&gt;). If it is, the hook checks for a session marker file. If the marker doesn't exist, the edit is denied:&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="nv"&gt;IS_COPY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;/src/app/&lt;span class="k"&gt;*&lt;/span&gt;.tsx|&lt;span class="k"&gt;*&lt;/span&gt;/src/components-next/&lt;span class="k"&gt;*&lt;/span&gt;.tsx&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;IS_COPY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;/src/articles/&lt;span class="k"&gt;*&lt;/span&gt;.md&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;IS_COPY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;/src/social/&lt;span class="k"&gt;*&lt;/span&gt;.md&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;IS_COPY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&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;$IS_COPY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&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;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;MARKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/voice-tone-reviewed-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&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="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&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;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When denied, the hook outputs JSON that Claude Code interprets as a hard block:&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;"hookSpecificOutput"&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;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecisionReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BLOCKED: Cannot edit copy file 'page.tsx' without voice &amp;amp; tone review. You MUST first delegate to voice-and-tone-lead using the Agent tool."&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;The AI cannot proceed. The denial message tells it exactly what to do: delegate to voice-and-tone-lead first.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The unlock (PostToolUse)
&lt;/h3&gt;

&lt;p&gt;After the AI calls the Agent tool, a PostToolUse hook checks whether the subagent was voice-and-tone-lead. If so, it creates the session marker:&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;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SUBAGENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;voice-and-tone-lead&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/voice-tone-reviewed-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marker is a file in &lt;code&gt;/tmp&lt;/code&gt; named with the session ID. Once the voice-and-tone-lead has reviewed, all subsequent edits to copy files in that turn are unblocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The reset (Stop)
&lt;/h3&gt;

&lt;p&gt;A Stop hook fires when the AI finishes responding, before control returns to the user. It removes the session marker so the next prompt requires a fresh voice review:&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;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/voice-tone-reviewed-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the reset, one early review would cover every edit for the rest of the session. With it, each turn starts locked. Each edit gets checked against the guide instead of relying on a single early review to cover everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reviewer
&lt;/h2&gt;

&lt;p&gt;The voice-and-tone-lead is a Claude Code agent defined in &lt;code&gt;.claude/agents/voice-and-tone-lead.md&lt;/code&gt;. It has read-only access to the codebase (Read, Glob, Grep) and no write permissions. It cannot edit files. It can only review.&lt;/p&gt;

&lt;p&gt;The agent definition is a markdown file with YAML front matter. Here is a trimmed version:&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="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;voice-and-tone-lead&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reviewer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;copy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Reads&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VOICE-AND-TONE.md"&lt;/span&gt;
  &lt;span class="s"&gt;and reviews proposed changes against the guide's voice principles, tone guidance,&lt;/span&gt;
  &lt;span class="s"&gt;banned patterns, and word list. Reports violations with suggested fixes.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Read&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Glob&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Grep&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

You are the Voice and Tone Lead. You review proposed copy changes against the
project's VOICE-AND-TONE.md guide before any user-facing text is edited. You are
a reviewer, not an editor.

&lt;span class="gu"&gt;## What You Check&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Voice principles (direct, confident, specific, empathetic)
&lt;span class="p"&gt;-&lt;/span&gt; Tone guidance for the relevant section type
&lt;span class="p"&gt;-&lt;/span&gt; Banned patterns
&lt;span class="p"&gt;-&lt;/span&gt; Word list (prefer/avoid)
&lt;span class="p"&gt;-&lt;/span&gt; Technical constraints (no em-dashes)

&lt;span class="gu"&gt;## Constraints&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; You are read-only. You do not edit files.
&lt;span class="p"&gt;-&lt;/span&gt; If the change is purely structural (no user-visible text changes), report PASS.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tools&lt;/code&gt; list is the key constraint. Read, Glob, and Grep give the agent enough access to review code without the ability to change it.&lt;/p&gt;

&lt;p&gt;If the copy passes, it reports PASS. If there are violations, it lists each one with the offending text, the rule it breaks, and a suggested fix. The AI then incorporates the fixes before writing.&lt;/p&gt;

&lt;p&gt;A typical review looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Voice &amp;amp; Tone Review: VIOLATIONS FOUND&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[Direct voice principle]&lt;/strong&gt; Line 4&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issue&lt;/strong&gt;: "We'd love to help you navigate the complexities" is hedging. The guide says "Say the thing. No preamble."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt;: "We'd love to help you navigate the complexities of AI integration."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: "I review your AI coding setup and tell you what's working and what isn't."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[Banned pattern: feature claims in fit checks]&lt;/strong&gt; Line 5&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issue&lt;/strong&gt;: "Reach out today to learn how our team can support your journey" sells the service instead of describing the visitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: "Book a call if that's useful."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reviewer is read-only by design. Separating the reviewer from the editor means the review happens before the edit, not after. The AI can't write first and review later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the guide looks like
&lt;/h2&gt;

&lt;p&gt;The reviewer checks every proposed change against the &lt;code&gt;VOICE-AND-TONE.md&lt;/code&gt; file. The more concrete the guide, the less room the reviewer has to drift. Here is what concreteness looks like in practice.&lt;/p&gt;

&lt;p&gt;The banned patterns table lists specific phrases the AI must avoid, with the reason each one fails:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Why it fails&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"actually" as emphasis&lt;/td&gt;
&lt;td&gt;Signals you expect to be doubted&lt;/td&gt;
&lt;td&gt;"I actually read your code"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Competitor bashing&lt;/td&gt;
&lt;td&gt;Positions you as the cheap alternative&lt;/td&gt;
&lt;td&gt;"Agencies charge $50k and take 8 weeks"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature claims in fit checks&lt;/td&gt;
&lt;td&gt;Sells the service instead of describing the visitor&lt;/td&gt;
&lt;td&gt;"You need someone who can implement guardrails"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each section of the site gets its own tone guidance. Here is the objection handling section:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tone: Respectful and substantive.&lt;/strong&gt; The reader asked a real question. Answer it with real information, not a dismissal.&lt;/p&gt;

&lt;p&gt;"You can. An AI audit will catch syntax and pattern issues. It won't catch the architectural gaps, the business logic that doesn't match your edge cases, or the dependencies that don't exist. That takes a human who's shipped production code."&lt;/p&gt;

&lt;p&gt;Not: "You can. But Claude wrote the bugs in the first place."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The guide also includes tone sections for distribution channels: LinkedIn, Twitter, Reddit, Hacker News, Dev.to, Lobsters, and Bluesky. Each channel has its own constraints. Reddit gets the substance upfront. Twitter compresses to one idea. Lobsters titles read like paper abstracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a gate, not a nudge
&lt;/h2&gt;

&lt;p&gt;A nudge injects a warning into the AI's context without blocking anything. The AI sees the message and can act on it, but nothing stops it from continuing. The &lt;a href="https://windyroad.com.au/blog/making-work-in-progress-visible-to-your-ai-agent" rel="noopener noreferrer"&gt;WIP accumulation hooks&lt;/a&gt; use nudges. Voice enforcement uses a gate because the failure mode is different.&lt;/p&gt;

&lt;p&gt;A missed WIP nudge means work accumulates longer than it should. That's recoverable. A missed voice review means off-brand copy ships to production. Fixing copy after the fact means noticing the drift, finding it, rewriting it, and redeploying. The cost of prevention (one agent call before editing) is lower than the cost of remediation.&lt;/p&gt;

&lt;p&gt;The gate fires on Edit and Write to specific file paths. It doesn't fire on CSS changes, configuration files, or backend code. The scope is narrow: files that contain user-facing copy.&lt;/p&gt;

&lt;p&gt;Other approaches exist. A post-commit linting step catches drift but only after the copy is written, meaning the AI has already moved on and a rewrite costs more context. A manual review checklist works but depends on the reviewer remembering to use it, which is the same problem the guide had. The gate catches drift at the point of edit, before the copy exists in the file, with no human discipline required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;The reviewer agent call adds 10-30 seconds per turn. For a single article or landing page edit, that's negligible. For bulk changes across many files in separate turns, it adds up.&lt;/p&gt;

&lt;p&gt;The marker resets after each turn, so every turn that touches copy gets a fresh review. Within a single turn, one review covers all edits. If the AI edits multiple copy files in one turn, only the first triggers the reviewer. The later edits in that same turn ride on the same approval. For most workflows this is fine: a single turn rarely drifts mid-response. For high-stakes copy (pricing pages, legal text), splitting edits across turns forces a review on each one.&lt;/p&gt;

&lt;p&gt;The reviewer itself is an AI agent, subject to the same drift it's checking for. It works because it re-reads the guide on every invocation rather than relying on memory, and because the guide is specific enough (banned patterns, a word list, concrete examples) to leave less room for interpretation.&lt;/p&gt;

&lt;p&gt;False positives and false negatives still happen. The reviewer might flag a sentence that's fine, or miss a subtle drift the guide doesn't explicitly cover. There's no automated way to audit reviewer quality. The practical check is reading the reviewer's output: if its reasoning references specific guide sections and quotes the offending text, it's doing its job. If it produces vague approvals ("looks good, no issues"), the guide probably isn't specific enough.&lt;/p&gt;

&lt;p&gt;When the reviewer flags something you disagree with, override it. The gate blocks the AI, not you. Claude Code prompts for confirmation on denied edits, and you can approve them directly. The reviewer is a check, not a veto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting this for your project
&lt;/h2&gt;

&lt;p&gt;Start with the guide. &lt;a href="https://styleguide.mailchimp.com/voice-and-tone/" rel="noopener noreferrer"&gt;Mailchimp's voice and tone guide&lt;/a&gt; is a good starting point. Ours began there and diverged as we identified patterns specific to this site. Write down how your site should sound. Be specific: include examples of what good copy looks like and what bad copy looks like. Banned patterns and a word list catch the most common drift.&lt;/p&gt;

&lt;p&gt;A minimal guide might start here:&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;## Voice principles&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Direct: short sentences, no preamble
&lt;span class="p"&gt;-&lt;/span&gt; Specific: use numbers and names, not adjectives

&lt;span class="gu"&gt;## Banned patterns&lt;/span&gt;
| Pattern | Why it fails |
|---------|-------------|
| "We're passionate about" | Generic, says nothing specific |
| "Leverage/utilize" | Jargon for "use" |

&lt;span class="gu"&gt;## Word list&lt;/span&gt;
| Use | Instead of |
|-----|-----------|
| use | leverage, utilize |
| help | empower, enable |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define the scope. Which files contain user-facing copy? In this project, it's &lt;code&gt;.tsx&lt;/code&gt; files in two directories and &lt;code&gt;.md&lt;/code&gt; files in two directories. Your project might be different. If you publish to multiple channels (LinkedIn, Twitter, Reddit), add tone sections for each one. The guide captures how the voice adapts per context so the AI doesn't flatten everything to one register.&lt;/p&gt;

&lt;p&gt;Create the agent. The voice-and-tone-lead agent definition is a markdown file in &lt;code&gt;.claude/agents/&lt;/code&gt;. It describes the role, lists what to check, and defines the output format. Give it read-only tools so it reviews but doesn't edit.&lt;/p&gt;

&lt;p&gt;Wire the four hooks. The detection hook goes in &lt;code&gt;UserPromptSubmit&lt;/code&gt;. The gate goes in &lt;code&gt;PreToolUse&lt;/code&gt; with a matcher for Edit and Write. The unlock goes in &lt;code&gt;PostToolUse&lt;/code&gt; with a matcher for Agent. The reset goes in &lt;code&gt;Stop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The full configuration is in the public repo at &lt;a href="https://github.com/windyroad/windyroad" rel="noopener noreferrer"&gt;github.com/windyroad/windyroad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;Claude Code hooks documentation&lt;/a&gt; covers the full event model and the hook output format.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Making work-in-progress visible to your AI agent</title>
      <dc:creator>Tom Howard</dc:creator>
      <pubDate>Mon, 09 Mar 2026 05:21:13 +0000</pubDate>
      <link>https://forem.com/tompahoward/making-work-in-progress-visible-to-your-ai-agent-5hjn</link>
      <guid>https://forem.com/tompahoward/making-work-in-progress-visible-to-your-ai-agent-5hjn</guid>
      <description>&lt;p&gt;An AI agent has no visibility into accumulating work-in-progress. It works on the current prompt. Meanwhile, uncommitted changes grow, commits pile up without a push, changesets go unwritten, and a release PR sits open for days. The accumulation happens silently.&lt;/p&gt;

&lt;p&gt;This matters because work-in-progress is risk. In Lean terms, it is internal inventory: work that has been started but has not yet delivered value to a customer. Uncommitted changes can be lost to a crash or a branch switch. Unpushed commits are invisible to the pipeline. Missing changesets mean no release PR will be created. A stale release PR means tested, reviewed code is sitting in a queue instead of running in production.&lt;/p&gt;

&lt;p&gt;I built a &lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; hook that surfaces all of this. Four checks monitor four queues where code accumulates on its way to production. Local checks run every prompt. Remote checks run on push. No blocking. The AI keeps working but both it and I can see the state of things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four queues
&lt;/h2&gt;

&lt;p&gt;Code flows through four queues between your editor and production. Each queue is a place where work can stall. Each check monitors one queue.&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.amazonaws.com%2Fuploads%2Farticles%2F4b0gg3vy1g17i4kilqfq.png" 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.amazonaws.com%2Fuploads%2Farticles%2F4b0gg3vy1g17i4kilqfq.png" alt="Flow diagram showing four queues that code passes through on its way to production: uncommitted changes in the working tree, unpushed commits ahead of origin, pushed commits with no release preview because no changeset file exists, and unreleased code sitting in an open release PR. A dashed vertical line separates the two local queues from the two remote queues. Below each queue, a label shows what monitors it." width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first two queues are local. Checking them requires only git commands against the local repo, which complete in milliseconds. The last two queues are remote. Checking them requires &lt;code&gt;gh&lt;/code&gt; CLI calls that hit the GitHub API, taking 500ms to 2 seconds each. Running those on every prompt would add noticeable latency, so they run once on push. The remote state only changes when you push, so there's no need to re-check between pushes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local checks (every prompt)
&lt;/h2&gt;

&lt;p&gt;The hook lives in &lt;code&gt;.claude/hooks/wip-nudge.sh&lt;/code&gt; and fires on &lt;code&gt;UserPromptSubmit&lt;/code&gt;, the same event used by the &lt;a href="https://windyroad.com.au/blog/enforcing-pipeline-discipline-with-claude-code-hooks" rel="noopener noreferrer"&gt;pipeline discipline hooks&lt;/a&gt;. Every check is independent. If one fails silently (no remote, no &lt;code&gt;gh&lt;/code&gt; CLI), the others still run.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Uncommitted changes too large
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DIFF_STAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff HEAD &lt;span class="nt"&gt;--stat&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git diff HEAD&lt;/code&gt; captures both staged and unstaged changes. The &lt;code&gt;--stat&lt;/code&gt; summary line looks like &lt;code&gt;5 files changed, 180 insertions(+), 42 deletions(-)&lt;/code&gt;. The script extracts the insertion and deletion counts and adds them. If the total is 200 or more:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WIP: ~222 lines of uncommitted changes. Consider committing before continuing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The threshold of 200 is a judgment call. Below that, you're mid-task. Above it, you have enough work that losing it would hurt.&lt;/p&gt;

&lt;p&gt;The check also counts lines in untracked files (excluding &lt;code&gt;.DS_Store&lt;/code&gt; and &lt;code&gt;node_modules&lt;/code&gt;) and adds them to the total. A 250-line file you haven't staged yet still counts toward the threshold.&lt;/p&gt;

&lt;p&gt;There's a related check for stale modifications. If a tracked file has been modified but not committed for more than 24 hours, the hook flags it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WIP: 2 modified file(s) uncommitted for over 24h. Forgotten or should be reverted?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A day-old uncommitted change is either forgotten work or something that should be reverted. Either way, it shouldn't sit there silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Unpushed commits piling up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;UNPUSHED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-list &lt;span class="nt"&gt;--count&lt;/span&gt; origin/master..HEAD 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the count is 3 or more:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WIP: 5 unpushed commits on master. Consider running &lt;code&gt;npm run push:watch&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One or two unpushed commits is normal mid-task flow. Three or more means multiple units of work are sitting locally. The pipeline can't run against them. The release PR can't include them. If the push eventually fails CI, you're debugging a larger delta than necessary.&lt;/p&gt;

&lt;p&gt;The nudge suggests &lt;code&gt;npm run push:watch&lt;/code&gt; rather than bare &lt;code&gt;git push&lt;/code&gt; because of the pipeline discipline hooks already wired into this project. That script pushes, watches the pipeline, and surfaces the deploy URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote checks (on push)
&lt;/h2&gt;

&lt;p&gt;Checks 3 and 4 run inside &lt;code&gt;scripts/push-watch.sh&lt;/code&gt; after the push completes and the remote refs are updated. The warnings print to stdout alongside the pipeline status and deploy URLs. Since the remote state only changes when you push, running the checks at push time is both the right moment and the only moment they need to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No release preview
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;UNRELEASED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-list &lt;span class="nt"&gt;--count&lt;/span&gt; origin/publish..origin/master 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;CHANGESET_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find .changeset &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'README.md'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;OLDEST_UNRELEASED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%aI'&lt;/span&gt; &lt;span class="nt"&gt;--reverse&lt;/span&gt; origin/publish..origin/master 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are pushed commits ahead of &lt;code&gt;publish&lt;/code&gt;, no changeset files, and the oldest of those commits is more than 24 hours old or there are 3 or more:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WIP: 8 unreleased commits with no changeset (oldest: 3 day(s) ago). Run &lt;code&gt;npx changeset&lt;/code&gt; to describe what's shipping.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without a changeset file, the &lt;a href="https://github.com/changesets/changesets" rel="noopener noreferrer"&gt;changesets&lt;/a&gt; action won't create a release PR. Changes accumulate on trunk with no release preview, no version bump, no CHANGELOG entry. This is internal inventory: work that has been done but is not flowing toward a release.&lt;/p&gt;

&lt;p&gt;The check fires on either condition: the oldest pushed commit is over 24 hours old, or there are 3 or more pushed commits without a changeset. A single recent commit is normal mid-task flow. Three commits in one day without a changeset is accumulation worth flagging, even if everything is fresh.&lt;/p&gt;

&lt;p&gt;The check compares &lt;code&gt;origin/publish..origin/master&lt;/code&gt; rather than &lt;code&gt;origin/publish..HEAD&lt;/code&gt;, limiting it to pushed commits and avoiding overlap with check 2. One changeset can cover multiple commits, so the check looks for the &lt;em&gt;existence&lt;/em&gt; of any changeset file, not a 1:1 mapping. Writing a changeset is a product decision: what's the change worth calling out, and how should it be described?&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unreleased code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PR_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;timeout &lt;/span&gt;10 gh &lt;span class="nb"&gt;pr &lt;/span&gt;list &lt;span class="nt"&gt;--base&lt;/span&gt; publish &lt;span class="nt"&gt;--state&lt;/span&gt; open &lt;span class="nt"&gt;--limit&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; number,url,createdAt 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[]"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an open PR targeting &lt;code&gt;publish&lt;/code&gt; exists and was created more than 24 hours ago, the nudge reminds you that tested, pipeline-verified code is waiting. The output includes a &lt;code&gt;CLAUDE:&lt;/code&gt; directive telling the AI to surface the release information and ask the user about it. The AI doesn't decide when to release, but it makes sure the human knows there's a release waiting:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WIP: Release PR #42 has been open for 3 day(s) with 5 changeset(s), ~1200 lines changed. &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;https://github.com/&lt;/a&gt;...&lt;br&gt;
CLAUDE: Tell the user about this release PR and ask if they want to review and merge it now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;CLAUDE:&lt;/code&gt; prefix is a convention, not a feature of Claude Code. Any text the AI sees in its context can influence its behavior. A labelled directive just makes the intent explicit.&lt;/p&gt;

&lt;p&gt;The check reports both changeset count and total diff size. A release with one changeset feels different from one with five, but a single changeset touching 2,000 lines is also worth knowing about. Both dimensions help decide whether to review now or keep working.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;gh&lt;/code&gt; CLI calls are wrapped in &lt;code&gt;timeout 10&lt;/code&gt; because they hit the network. If GitHub is slow or unreachable, the script fails with an error rather than silently skipping the check. A silent skip would mean you get no feedback about the release PR, with no indication that the check didn't run. Failing loudly means you know immediately if something is wrong with your GitHub token or network connectivity.&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.amazonaws.com%2Fuploads%2Farticles%2F2anm8vhg4yuf110upo30.png" 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.amazonaws.com%2Fuploads%2Farticles%2F2anm8vhg4yuf110upo30.png" alt="Four numbered cards showing each check: uncommitted changes threshold at 200 lines, unpushed commits threshold at 3, unreleased commits without changeset files compared against the publish branch, and stale release PR when open longer than 24 hours. Below, a flow diagram showing the hook firing on every prompt, running checks, emitting additionalContext and a systemMessage, and the AI plus human seeing the result." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Informing without blocking
&lt;/h2&gt;

&lt;p&gt;Every warning line is appended to a single &lt;code&gt;additionalContext&lt;/code&gt; string in the hook's JSON output, alongside a &lt;code&gt;systemMessage&lt;/code&gt; that prints directly in the terminal. The pattern is the same one used by other &lt;a href="https://windyroad.com.au/blog/enforcing-pipeline-discipline-with-claude-code-hooks" rel="noopener noreferrer"&gt;pipeline discipline hooks&lt;/a&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "systemMessage": "WIP accumulation detected. See warnings below.",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": &lt;/span&gt;&lt;span class="nv"&gt;$ESCAPED&lt;/span&gt;&lt;span class="sh"&gt;
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;permissionDecision: "deny"&lt;/code&gt;. &lt;code&gt;systemMessage&lt;/code&gt; prints in your terminal every time the hook fires. &lt;code&gt;additionalContext&lt;/code&gt; injects the detail into the AI's context. You see the warning. The AI has the detail. If multiple checks fire, all warnings stack into the same output.&lt;/p&gt;

&lt;p&gt;The push gate in the &lt;a href="https://windyroad.com.au/blog/enforcing-pipeline-discipline-with-claude-code-hooks" rel="noopener noreferrer"&gt;pipeline discipline hooks&lt;/a&gt; is a hard block because &lt;code&gt;git push&lt;/code&gt; without pipeline visibility is the specific action I want to prevent. WIP accumulation is different. It's state you want to be aware of, not an action you want to block. A gate that blocks every edit until you commit would be counterproductive during a multi-file change.&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.amazonaws.com%2Fuploads%2Farticles%2Fg20neggwr19yma1stjl2.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fg20neggwr19yma1stjl2.png" alt="Terminal window showing example output from the hook: four warning lines covering uncommitted changes, unpushed commits, missing changeset, and stale release PR. Below, a note that Claude continues working normally with no blocking." width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up
&lt;/h2&gt;

&lt;p&gt;The prompt-time hook goes in &lt;code&gt;.claude/settings.json&lt;/code&gt; alongside the other &lt;code&gt;UserPromptSubmit&lt;/code&gt; hooks:&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;"UserPromptSubmit"&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;"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;".claude/hooks/project-health-check.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="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;".claude/hooks/wip-nudge.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;Each &lt;code&gt;UserPromptSubmit&lt;/code&gt; entry runs independently. The health check and the WIP nudge don't know about each other. They both emit hook output if they have something to say, and stay silent if they don't.&lt;/p&gt;

&lt;p&gt;The remote checks run inside &lt;code&gt;push:watch&lt;/code&gt;, the same script that pushes and watches the pipeline. After the push completes and the remote refs are updated, the script runs checks 3-4 and prints any warnings to stdout. Since the AI is already watching the push output, the warnings are visible in the conversation without any caching mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full hook
&lt;/h2&gt;

&lt;p&gt;The complete local hook (&lt;code&gt;.claude/hooks/wip-nudge.sh&lt;/code&gt;) and the remote checks in &lt;code&gt;scripts/push-watch.sh&lt;/code&gt; are in the public repo at &lt;a href="https://github.com/windyroad/windyroad" rel="noopener noreferrer"&gt;github.com/windyroad/windyroad&lt;/a&gt;. The key snippets are all shown above. The full scripts add error handling, JSON escaping for the &lt;code&gt;additionalContext&lt;/code&gt; output, and the stale-file detection using &lt;code&gt;python3&lt;/code&gt; to compare file modification times against a 24-hour threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting this for your project
&lt;/h2&gt;

&lt;p&gt;The specific checks here are tuned for a trunk-based workflow with changesets and a &lt;code&gt;publish&lt;/code&gt; branch. The pattern works for any accumulation you want to track.&lt;/p&gt;

&lt;p&gt;To build your own version, start by mapping your queues. Where does code accumulate on its way to production? Uncommitted changes, unpushed commits, and missing changesets are common. You might also check for TODO comments in uncommitted code, failing local tests, stale feature branches, or a growing number of skipped tests.&lt;/p&gt;

&lt;p&gt;Split checks by cost. Anything that uses only local git commands belongs in the prompt-time hook. Anything that hits the network (GitHub API, CI status, deployment state) belongs in the script you use instead of bare &lt;code&gt;git push&lt;/code&gt;. In this project, that's &lt;code&gt;push:watch&lt;/code&gt;, which already pushes, watches the pipeline, and surfaces deploy URLs. Adding the remote WIP checks there means they run at the moment the remote state changes, without adding a separate mechanism.&lt;/p&gt;

&lt;p&gt;Set thresholds that match your rhythm. The 200-line and 3-commit thresholds here reflect a workflow where commits are small and pushes are frequent. If your commits are larger or your pushes are batched, adjust accordingly.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;additionalContext&lt;/code&gt;, not &lt;code&gt;permissionDecision&lt;/code&gt;. The warning is in the AI's context when it generates its next response. The AI factors it into what it does next without being blocked.&lt;/p&gt;

&lt;p&gt;Make every check independent. Each check should succeed or fail silently on its own. Use &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; and &lt;code&gt;|| echo "0"&lt;/code&gt; so that a missing remote or absent CLI tool doesn't break the other checks.&lt;/p&gt;

&lt;p&gt;Keep the prompt-time hook fast. It runs on every prompt. The git commands are local and near-instant. Network calls belong in the push script, not the prompt hook.&lt;/p&gt;

&lt;p&gt;Wire it into &lt;code&gt;UserPromptSubmit&lt;/code&gt; by adding an entry to the &lt;code&gt;hooks&lt;/code&gt; object in your &lt;code&gt;.claude/settings.json&lt;/code&gt;. The hook receives the prompt as JSON on stdin (which this script ignores) and emits the hook JSON on stdout.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;Claude Code hooks documentation&lt;/a&gt; covers the full event model: &lt;code&gt;UserPromptSubmit&lt;/code&gt; for pre-prompt checks, &lt;code&gt;PreToolUse&lt;/code&gt; for intercepting tool calls, and &lt;code&gt;PostToolUse&lt;/code&gt; for post-action verification.&lt;/p&gt;

&lt;p&gt;The full hook configuration for this site is in the public repo at &lt;a href="https://github.com/windyroad/windyroad" rel="noopener noreferrer"&gt;github.com/windyroad/windyroad&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>devops</category>
      <category>cicd</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
