<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tatsuya Shimomoto</title>
    <description>The latest articles on DEV Community by Tatsuya Shimomoto (@shimo4228).</description>
    <link>https://dev.to/shimo4228</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3772086%2F7b113abd-2f92-4728-993c-602762a15288.png</url>
      <title>DEV Community: Tatsuya Shimomoto</title>
      <link>https://dev.to/shimo4228</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shimo4228"/>
    <language>en</language>
    <item>
      <title>What Humans Should Approve Is Intent, Not the Diff — A Decision Table for Agent Approval Gates</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Wed, 29 Jul 2026 03:41:20 +0000</pubDate>
      <link>https://dev.to/shimo4228/what-humans-should-approve-is-intent-not-the-diff-a-decision-table-for-agent-approval-gates-1a3j</link>
      <guid>https://dev.to/shimo4228/what-humans-should-approve-is-intent-not-the-diff-a-decision-table-for-agent-approval-gates-1a3j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: How to catch drift from your intent &lt;strong&gt;while it's still cheap to undo&lt;/strong&gt; (just before commit or publish) without slowing your agent's autonomous execution down. You get a &lt;strong&gt;decision table that mechanically determines, from the kind of thing being changed&lt;/strong&gt;, whether the gate should show a human the full diff text or just an intent summary — plus &lt;strong&gt;the one required item that keeps the summary honest: a three-valued &lt;code&gt;Divergence from plan&lt;/code&gt; declaration&lt;/strong&gt;. It ports directly into your own workflow or your team's code review conventions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you extend how far an agent runs on its own, you eventually hit this fork.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lean toward not stopping, and drift from intent only becomes visible after the fact.&lt;/strong&gt; The thing works, but it's pointed the wrong way. By the time you notice, it has piled up and you can't afford to unwind it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lean toward stopping, and the human becomes the bottleneck.&lt;/strong&gt; A review queue forms, and the point of autonomous execution evaporates&lt;/li&gt;
&lt;li&gt;So you compromise: "let's at least show the diff." &lt;strong&gt;This is the worst of the three.&lt;/strong&gt; You're paying the cost of stopping, but the volume means nobody reads it, and intent still isn't protected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I suspect the third one is the most widespread. The gate survives as a formality, and in substance you're back to the first case.&lt;/p&gt;

&lt;p&gt;Here's what to re-examine. &lt;strong&gt;Who is that automation for?&lt;/strong&gt; If going fast is itself the goal, humans are in the way — but what you actually want is for &lt;em&gt;the thing you intended&lt;/em&gt; to get built fast. If so, what to cut is not human involvement itself, but &lt;strong&gt;what the human is involved in&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So this article &lt;strong&gt;keeps the number of times a human is stopped the same, and moves only the layer they judge at&lt;/strong&gt;. Whether the artifact is correct becomes the machine's primary responsibility, and the human holds the layer above it: what this is aiming at, and what changes as a result. That changes what the gate shows. That routing is the decision table below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The examples come from Claude Code 2.1.220 plus my own harness (the set of rules and skills under &lt;code&gt;~/.claude&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;But the deliverable is &lt;strong&gt;a way of writing conventions&lt;/strong&gt;, so it isn't tool-dependent. It works the same in Cursor, in Devin, or in your company's code review policy&lt;/li&gt;
&lt;li&gt;Background assumed: you have an AI agent implement things, and you have some approval step before commit&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gates have two axes
&lt;/h2&gt;

&lt;p&gt;When you design an approval gate, there are actually two independent questions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What decides it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;First axis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;When&lt;/strong&gt; do you stop&lt;/td&gt;
&lt;td&gt;Reversibility. An edit you can undo with &lt;code&gt;git checkout&lt;/code&gt; doesn't stop; publishing externally, minting a DOI, or committing does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Second axis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When you've stopped, &lt;strong&gt;what&lt;/strong&gt; does the human judge&lt;/td&gt;
&lt;td&gt;The kind of target (the subject of this article)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Usually only the first axis is settled. "Stop before commit." "Confirm before publishing." — &lt;strong&gt;the stop condition is written down, but what to show when you stop isn't.&lt;/strong&gt; My own harness was like that.&lt;/p&gt;

&lt;p&gt;That blank doesn't stay blank. Every time you write a gate, it gets filled in by whatever interpretation is handy at that moment, and the default it fills in with is almost always "show the diff." From the writer's side that looks safest. Show everything and nothing gets missed. The result is the third case from the opening, mass-produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision table: what you show is decided by the target
&lt;/h2&gt;

&lt;p&gt;This is the deliverable. &lt;strong&gt;When a gate stops, what you put in front of the human is determined by the kind of thing being changed.&lt;/strong&gt; (Below, "deterministic gate" means checks a machine can answer Yes/No on: lint, type checking, tests, secret scan.)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;What to show&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Behavior-shaping artifacts&lt;/strong&gt;&lt;br&gt;(&lt;code&gt;CLAUDE.md&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt; / rules / skill and agent definitions / public documentation)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The text itself &lt;em&gt;is&lt;/em&gt; the intent. Reading it is already work at the intent layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Control plane&lt;/strong&gt;&lt;br&gt;(hooks / permission settings / permission definitions like &lt;code&gt;--allowedTools&lt;/code&gt; / scheduled task definitions)&lt;br&gt;&lt;strong&gt;and artifacts that produce the evidence&lt;/strong&gt; the checks run on (tests / fixtures / lint config / coverage thresholds / CI definitions / review agent prompts / dependencies)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;These move the gate itself, and &lt;strong&gt;the evidence the gate rests on&lt;/strong&gt;. Fold them into a summary and "a change that loosens the checks" disappears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Implementation code and generated output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Intent summary&lt;/strong&gt;&lt;br&gt;(what it aims at, and what changes as a result)&lt;/td&gt;
&lt;td&gt;Correctness that mechanized checks can decide belongs to deterministic gates and review agents. The diff text and the PASS list don't go on the approval screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When a deterministic gate FAILs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The detection line itself&lt;/strong&gt;&lt;br&gt;(mask only the secret's actual value)&lt;/td&gt;
&lt;td&gt;This is a state absent from the approved plan, so there's nothing to fold a summary into (the cross-check mechanism is described later). Bypassing it is a decision to disable one check, which puts it in the same class as the control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Escalation rule: irreversibility overrides the target category
&lt;/h3&gt;

&lt;p&gt;The table is a &lt;strong&gt;principle&lt;/strong&gt;, and it doesn't decide everything on its own. Even within "implementation code," padding tweaks in a UI and changes to authorization logic, DB migrations, billing, data deletion, or key rotation call for different things to hand a human. So add a one-line override.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;For irreversible, high-impact changes, show the full text (or the relevant diff) regardless of target category.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is to say the first axis (reversibility) affects not only "when you stop" but also "what you show." &lt;strong&gt;The principle is decided by the kind of target, and irreversibility escalates it toward showing full text&lt;/strong&gt; — that two-stage form is the accurate statement. No third axis is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  In one line
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Correctness that mechanized checks can decide belongs to the machine; the human holds intent.&lt;/strong&gt; The one exception is targets where the text &lt;em&gt;is&lt;/em&gt; the intent — there, reading the text is itself the intent judgment, so you show the text. &lt;strong&gt;Showing full text isn't a backslide into the old way; it's the same principle showing up differently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's one more principle you need alongside the decision table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A review agent is an inspector, not an approver.&lt;/strong&gt; If the side proposing and the side inspecting come from the same lineage, the inspection inherits the proposer's blind spots wholesale (this article calls that the generator–verifier gap). So approval is composed of two things, "the deterministic gate PASSed" plus "the human's intent judgment," and &lt;strong&gt;you never build a path where approval closes on an LLM alone&lt;/strong&gt;. Making review heavier does not mean approval can be delegated to an LLM.&lt;/p&gt;

&lt;p&gt;Written into a conventions file, it looks like this (excerpted from the real thing; internal links and some references are omitted).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Artifacts are the machine's, intent is the human's

Correctness that mechanized checks can decide belongs to deterministic gates and review agents;
**do not put humans back on artifact inspection** (an assignment of primary responsibility, not a
guarantee — residual risk is caught by the escalation rule). A review agent is **an inspector, not
an approver** (generator–verifier gap). Approval is composed of "the deterministic gate PASSed"
plus "the human's intent judgment"; **never create an approval path that closes on an LLM alone.**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Applying it: what was actually happening in five places
&lt;/h2&gt;

&lt;p&gt;Before I made the decision table, my harness had the second axis blank. What follows is my own environment, but &lt;strong&gt;any setup with the same blank will produce the same shape&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same gate had two competing sources of truth
&lt;/h3&gt;

&lt;p&gt;For the intervention point just before commit, two files said different things. Nowhere was it written which one was canonical (i.e. which one you ultimately follow).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it said&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rules/common/planning.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;Verify results check&lt;/strong&gt; — just before commit" (what to show was unspecified)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skills/implementation-chain/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;diff approval&lt;/strong&gt; just before publish / deposit / commit"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The origin was a single sentence I'd written earlier in a record of a different design decision.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I just do a final check before commit&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It doesn't say what the "final check" is &lt;strong&gt;of&lt;/strong&gt;. Downstream, that sentence got read as "diff approval," and settled in wearing the face of a canonical rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  "A human inspects the artifact" was scattered across five places
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Wording before&lt;/th&gt;
&lt;th&gt;Result of applying the decision table&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;implementation-chain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"diff approval just before commit"&lt;/td&gt;
&lt;td&gt;Implementation code → changed to &lt;strong&gt;intent summary&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readme-writer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"a human approves the diff and applies it"&lt;/td&gt;
&lt;td&gt;Public documentation → rewritten as &lt;strong&gt;show full text&lt;/strong&gt; (same target, different reason)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;release-doi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;visually confirm&lt;/strong&gt; the latest commit is newer than the previous release"&lt;/td&gt;
&lt;td&gt;A predicate a machine can check → &lt;strong&gt;hand it to the machine&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paper-deposit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open the generated PDF's first page and &lt;strong&gt;eyeball&lt;/strong&gt; it (paraphrased)&lt;/td&gt;
&lt;td&gt;Same → replaced with machine verification of font embedding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;harness-sync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"&lt;code&gt;git diff&lt;/code&gt; is the review gate"&lt;/td&gt;
&lt;td&gt;What's being synced is rules / skills → &lt;strong&gt;leave it as is&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those five were written on different days, for different purposes. &lt;strong&gt;All of them landed on the same default with no coordination&lt;/strong&gt;, so this isn't individual carelessness. Wherever there's a blank, "show the human everything" fills it. Which is why patching the individual wording wouldn't stop it — I needed one convention covering the second axis itself.&lt;/p&gt;

&lt;p&gt;And that rightmost column shows where the decision table earns its keep. Grep and all five look identical, but &lt;strong&gt;wiping them out uniformly would take out gates you need&lt;/strong&gt;. &lt;code&gt;harness-sync&lt;/code&gt; did end up staying: what it syncs is rules and skills, so reading the text is itself the intent judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reinforcement 1: the control plane and "artifacts that produce the evidence"
&lt;/h2&gt;

&lt;p&gt;In the first version of the decision table, I put hooks and permissions on the "implementation code and config" side. A review from an external model (Codex CLI) hit exactly there as its top-priority finding.&lt;/p&gt;

&lt;p&gt;Codex's finding&lt;/p&gt;

&lt;p&gt;&amp;gt; The central target boundary permits executable harness controls to be hidden behind proposer-authored summaries, and the new rule conflicts with the existing security false-positive workflow.&lt;br&gt;
&amp;gt; - [P1] Classify executable harness controls as behavior-shaping — …&lt;/p&gt;

&lt;p&gt;Suppose the agent disabled the pre-commit secret scan hook. It can &lt;strong&gt;follow the convention to the letter&lt;/strong&gt; and still summarize it like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent summary: Tidied up the pre-commit check configuration and strengthened the checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The control plane "moves the gate itself," so folding it into a summary turns &lt;strong&gt;a change that loosens the checks into a well-meaning sentence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And hooks and permissions aren't the only way to weaken things. &lt;strong&gt;Rewriting the tests so they pass against your implementation&lt;/strong&gt; is the quietest path of all. Here the agent doesn't even need to lie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent summary: Fixed the implementation; all tests PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly true. The one thing that never surfaces in the summary is that what got rewritten was the tests. &lt;strong&gt;Not just "the tests passed" but "what counts as passing" is under review.&lt;/strong&gt; Fixtures, coverage thresholds, lint config, CI definitions, review agent prompts, and dependencies all have the same property.&lt;/p&gt;

&lt;h3&gt;
  
  
  So how should you present it
&lt;/h3&gt;

&lt;p&gt;The answer is: &lt;strong&gt;don't choose summary-or-text per commit&lt;/strong&gt;. If one commit contains both implementation and evidence, &lt;strong&gt;show the summary, and alongside it show the full text only for the parts that are evidence&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent summary: Changed the login-failure retry limit from 5 to 3 (plan step 2)

⚠ This change touches the evidence the checks rest on — review the text below

  tests/test_auth.py:41
  -    assert client.retry_limit == 5
  +    assert client.retry_limit == 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human reads only those last two lines. The implementation-side diff isn't shown. &lt;strong&gt;All you have to look at is whether the tests were loosened to match the implementation&lt;/strong&gt;, so volume can't beat you. The example above corresponds to a change decided in the plan, so it goes through. If the plan said nothing about retries and only the test threshold dropped, that's where you stop.&lt;/p&gt;

&lt;p&gt;Which files are on the evidence side can usually be picked up from the path.&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;# Extract candidates for "produces the evidence" from changed files, to show as full text&lt;/span&gt;
git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'^(tests?/|\.github/workflows/|\.claude/|hooks/)|(_test\.|\.test\.|pyproject\.toml|package\.json|ruff\.toml|codecov\.yml)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is not a complete classifier; it's a conservative candidate extractor.&lt;/strong&gt; It misses tests embedded in ordinary code, custom directories, lockfiles, and inline snapshots. Add the evidence-side files you know about in your own repo and grow it.&lt;/p&gt;

&lt;p&gt;In my environment, this extraction has moved out of the convention's prose and into a &lt;strong&gt;PreToolUse hook&lt;/strong&gt; (&lt;code&gt;evidence-file-notice.sh&lt;/code&gt;). When it detects a &lt;code&gt;git commit&lt;/code&gt;, it scans the staged files against a pattern like the one above (the real list is a bit broader — it also covers &lt;code&gt;spec/&lt;/code&gt;, &lt;code&gt;conftest.py&lt;/code&gt;, pre-commit config, and so on), and if anything matches, it injects the instruction itself into the approval flow: show the human the diff of these files. The canonical home of the evidence-file enumeration is no longer the convention's text but that one regex in the hook; the convention keeps a single pointer line saying detection is canonical there.&lt;/p&gt;

&lt;p&gt;There are two reasons for this restructuring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A convention that exists only in a document is followed only probabilistically.&lt;/strong&gt; Write "evidence-side files get their text shown" into a convention and there's no guarantee the agent recalls it every time. "Which files are evidence-side," though, is a structural property decided purely by the shape of the path — a machine fires on it 100% of the time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the enumeration in two places — convention and hook — and they will drift apart.&lt;/strong&gt; Centralize it on the side that fires, and let the convention hold only the principle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Semantic judgments like the decision table stay in the document; anything mechanically decidable gets lowered into a gate as soon as you spot it — this convention itself has been growing by exactly that process.&lt;/p&gt;

&lt;p&gt;The control plane (hooks / permissions / scheduled tasks) works the same way: summary plus the full text of the relevant files. &lt;strong&gt;Anything producing the evidence a judgment rests on gets treated the same as the judgment itself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reinforcement 2: on FAIL, show the detection line
&lt;/h2&gt;

&lt;p&gt;The second thing that same review found. Push "no machine-check PASS list and no diff text goes to the human" all the way, and &lt;strong&gt;you also delete the path where a human judges a secret scan false positive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In my harness, when the pre-commit secret scan detects something, there's a workflow to let it through with &lt;code&gt;SECRET_SCAN_BYPASS=1&lt;/code&gt;. The judgment is "this is a dummy key in a test, so it's fine."&lt;/p&gt;

&lt;p&gt;The reason this goes to a human isn't that a machine can't tell the difference. It's that &lt;strong&gt;even if the machine or the review agent happens to be right, this is the kind of decision whose owner must be a human&lt;/strong&gt;. A bypass is the act of disabling one check on the spot. That has the same nature as moving the control plane, so for the same reason as Reinforcement 1, you need the full text (i.e. the detection line).&lt;/p&gt;

&lt;p&gt;There's a second reason: &lt;strong&gt;a FAIL is a state absent from the approved plan.&lt;/strong&gt; An intent summary is meant to be read against the plan (see below), so a state that isn't in the plan has nothing to fold into.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**FAIL is the exception** — a deterministic gate's FAIL presents **the detection line itself**.
Mask the secret's actual value; the owner of the false-positive call (`*_BYPASS`) is the human
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PASS needs care. PASS only means "the checks you configured accepted this." &lt;strong&gt;Whether it was implemented according to the plan is not something PASS can tell you.&lt;/strong&gt; That's exactly why you need a separate intent summary — you leave out the PASS list &lt;em&gt;because&lt;/em&gt; you're looking at the summary instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Not shown" is not "not kept"
&lt;/h3&gt;

&lt;p&gt;This gets misread easily, so let me be explicit. Leaving out the PASS list means &lt;strong&gt;moving it off the human's approval screen&lt;/strong&gt;, not throwing check results away. Delete the trail and you lose any way to trace "what was passing back then" later. The idea in this article isn't to delete information; it's to &lt;strong&gt;move unneeded information off the human's judgment surface&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I originally had this distinction written into the convention as its own clause; the current version drops it. Claude Code already persists the transcript and tool results in machine-readable form at all times, so writing it down merely restated a default the runtime already guarantees. &lt;strong&gt;If your execution environment doesn't guarantee log retention, keep it as an explicit clause.&lt;/strong&gt; "Don't write into the convention what the substrate already guarantees" is itself one of the judgment calls you'll make when porting this.&lt;/p&gt;

&lt;p&gt;One more thing: on a true positive, dumping the detection line as-is duplicates the secret's actual value into the conversation, the approval screen, and the logs. That would spread the very leak the gate exists to prevent, so &lt;strong&gt;show the file, the rule name, and the surrounding context, and mask only the value&lt;/strong&gt;. What a human needs to judge a false positive is not the value itself, but where it was and which rule it hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reinforcement 3: don't let the intent summary be free-form
&lt;/h2&gt;

&lt;p&gt;This is a prerequisite for putting the decision table into practice.&lt;/p&gt;

&lt;p&gt;If you stop at "implementation code gets an intent summary," the human ends up &lt;strong&gt;judging by reading a self-report written by the proposer itself&lt;/strong&gt;. That just relocates the gap that was at the artifact layer. What they read changed from a diff to an essay; the author is the same.&lt;/p&gt;

&lt;p&gt;The countermeasure is to &lt;strong&gt;fix what the summary is cross-checked against to something human-originated&lt;/strong&gt;. My harness has two intervention points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plan check&lt;/strong&gt; — when what to do has been settled (this is where the human approves)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent check&lt;/strong&gt; — just before commit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the summary in 2 is &lt;strong&gt;presented cross-checked against the plan the human approved in 1&lt;/strong&gt;. Because the referent is a human-approved object, the loop can't close on self-reporting alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Only one item is required: the &lt;code&gt;Divergence from plan&lt;/code&gt; declaration
&lt;/h3&gt;

&lt;p&gt;But "write it against the plan" still leaves room to write it conveniently. In particular, &lt;strong&gt;when something new is discovered during implementation and the work naturally diverges from the plan&lt;/strong&gt;, that divergence quietly disappears from the summary.&lt;/p&gt;

&lt;p&gt;My first countermeasure was to make the intent summary itself a fixed form: five mandatory headings — approved intent, what changed, divergence from plan, impact on users and operations, evidence-side changes. But the day after that form went into the convention, an audit re-examining the whole convention clause by clause forced me to re-sort those five. &lt;strong&gt;Only one of the fields was actually preventing the worst case — the silent disappearance of divergence.&lt;/strong&gt; This is all that's required now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent summary: Changed the login-failure retry limit from 5 to 3 (plan step 2)
Divergence from plan: None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Divergence from plan&lt;/code&gt; takes one of three values: &lt;code&gt;None&lt;/code&gt; / &lt;code&gt;Yes&lt;/code&gt; / &lt;code&gt;Needs re-approval&lt;/code&gt;. The plan changing because of something you discovered mid-implementation isn't bad in itself. What's dangerous is the fact that it changed disappearing from the summary.&lt;/p&gt;

&lt;p&gt;And making this declaration required &lt;strong&gt;turns omission into falsehood&lt;/strong&gt;. In free-form text, a summary that doesn't mention the divergence isn't lying — it just didn't bring it up. With the declaration required, writing &lt;code&gt;None&lt;/code&gt; when there is divergence is a falsehood, and writing &lt;code&gt;Yes&lt;/code&gt; points the human's eyes exactly there. It closes the omission escape route structurally, without leaning on the writer's honesty. &lt;strong&gt;This is the one thing only formal enforcement can protect.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So why drop the other four fields? Because they were the summary's &lt;strong&gt;shape&lt;/strong&gt;, not its defense. What was intended, what changed, what's affected — summarizing without dropping those is exactly &lt;strong&gt;the calibration a model should bring to any summary&lt;/strong&gt;: lead with the outcome, report results faithfully, match the response to the question. Impose a fixed form on the parts calibration already covers, and you're forcing five headings onto a one-line config change — the approval screen turns into template skimming.&lt;/p&gt;

&lt;p&gt;That would reproduce, on the summary side, the third case I called the worst at the start: a gate that survives as a form nobody reads.&lt;/p&gt;

&lt;p&gt;(As for the "evidence-side changes" field — the hook from Reinforcement 1 covers it structurally, so there was never a need to make a human write it.)&lt;/p&gt;

&lt;p&gt;Anthropic's context-engineering guidance for the Claude 5 generation (&lt;a href="https://x.com/trq212/status/2080710971228918066" rel="noopener noreferrer"&gt;The new rules of context engineering for Claude 5 models&lt;/a&gt;) states this as a general rule: shift from binding with rules to delegating to judgment — &lt;strong&gt;except in the regions where the worst case is unacceptable, which stay explicitly bound&lt;/strong&gt;. The 5→1 reduction is that rule applied. Delegate the summary's shape to the model's calibration; bind exactly one point with form — the disappearance of divergence, the worst case you can't accept.&lt;/p&gt;

&lt;p&gt;The whole point of this gate convention was to concentrate the human's cognitive budget on judging intent. &lt;strong&gt;If the convention itself burns the writer's and reader's attention on enforcing a form, that defeats the purpose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One more practical upside: the three values are an enumeration, machine-readable, so "does the declaration exist at all" can itself be lowered into a hook check later. Same as Reinforcement 1 — build the convention so it has an exit ramp down into a deterministic gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cap the number of stops too: one gate per unit of work
&lt;/h3&gt;

&lt;p&gt;Paired with what to show, fix one rule about how often. The intent check happens &lt;strong&gt;once, at the completion point of a unit of work&lt;/strong&gt;. If a commit, a push, a publish, and the accompanying doc updates all arrive together, bundle them into &lt;strong&gt;one decision with the count and scope explicitly enumerated&lt;/strong&gt;. Ask for approval at every intermediate phase or intermediate commit and the approvals themselves become noise — the gate degrades into formality. There are exactly two exceptions: a deterministic gate FAILing, and &lt;code&gt;Divergence from plan: Needs re-approval&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But "bundling is allowed" is not "implicit is allowed." If one decision covers N items, say N at approval time. The moment an approval granted for one item gets silently reused for N, it has stopped being a gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raise the granularity and the bottleneck moves
&lt;/h3&gt;

&lt;p&gt;This cross-check bites hardest when the agent is good. &lt;strong&gt;Things going well for a long stretch is exactly the condition under which the intent summary becomes a formality.&lt;/strong&gt; If you find yourself skimming past the same "no divergence" every time, raise the granularity of the plan you're checking against.&lt;/p&gt;

&lt;p&gt;But that isn't free. The finer the plan, &lt;strong&gt;the further the bottleneck moves from just-before-commit to plan approval&lt;/strong&gt;. Reserve the finer granularity for the things you can't take back when they drift (external publishing, data migration, permission changes). You don't get to erase the fork entirely — the accurate statement is that &lt;strong&gt;you get to choose where you pay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On the conventions side, I also renamed the second intervention point from "Verify results check" to "intent check." Leave the name as "Verify results" and it drags you back into making humans read a list of machine-check results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Porting it into your own conventions: three steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enumerate your existing approval steps and check what each one says to present.&lt;/strong&gt; Grep for wording like "approve the diff," "visually confirm," "review and apply"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the decision table and the escalation rule into a single conventions file, and have each place you found point at it.&lt;/strong&gt; If you only patch the individual wording, the next skill or document you write will fill the blank the same way again&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the &lt;code&gt;Divergence from plan&lt;/code&gt; declaration (&lt;code&gt;None&lt;/code&gt; / &lt;code&gt;Yes&lt;/code&gt; / &lt;code&gt;Needs re-approval&lt;/code&gt;) into the summary as a required item.&lt;/strong&gt; The summary as a whole doesn't need a fixed form. Without this, you can install the decision table and still be verifying the proposer's own self-report&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;In step 1's grep, &lt;strong&gt;watch out for missing word stems&lt;/strong&gt;. I searched for &lt;code&gt;eyeballed&lt;/code&gt; and missed &lt;code&gt;eyeball&lt;/code&gt; (the uninflected stem), so one place survived all the way to the external model's review. Search verbs by stem rather than inflected form, or run &lt;code&gt;grep -i&lt;/code&gt; with several patterns.&lt;br&gt;
For reference, in my environment this change (one new conventions file plus re-pointing references across 13 related files) came to 14 files and 217 added lines. The decision table itself is short; what made the difference was &lt;strong&gt;re-wiring everything that referenced it&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The convention's body has since been cut in half (the text auto-loaded into every session's context went from 265 to 125 words). What remains is only the principles — the decision table, the escalation rule, the divergence declaration. The "why" moved into the decision record (ADR), and the evidence-file enumeration into the hook. Left alone, a convention fattens on rationale and examples until nobody reads it. &lt;strong&gt;Principles in the document, why in the record, anything machine-decidable in a gate&lt;/strong&gt; — splitting the storage that way is what keeps it maintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits of "the machine holds it"
&lt;/h2&gt;

&lt;p&gt;Let me be straight about this. &lt;strong&gt;Build, types, lint, tests, and secret scan tell you only about the properties they cover.&lt;/strong&gt; Holes in authorization logic, concurrency races, irreversible side effects of a migration, requirements that were never implemented at all — all of that sails through as PASS. And the review agent, being from the same lineage as the proposer, shares its blind spots (the generator–verifier gap above).&lt;/p&gt;

&lt;p&gt;So the third row of the decision table isn't "the machine guarantees correctness" — it's an operational tradeoff that &lt;strong&gt;puts primary responsibility for artifact inspection on the machine&lt;/strong&gt;. Residual risk does not go to zero.&lt;/p&gt;

&lt;p&gt;Which is exactly why you need the escalation rule. &lt;strong&gt;Keep the human on the artifact side only in the regions you can't undo&lt;/strong&gt; — data migration, permissions and billing, external publishing, deletion. Put another way: if a human is reading diffs of reversible implementation code, that isn't a response to residual risk, it's just inertia.&lt;/p&gt;

&lt;p&gt;You also can't reduce the whole convention to a deterministic lint. Extracting the evidence-side files (the hook in Reinforcement 1) and checking that the divergence declaration exists can be mechanized, but deciding "is this a behavior-shaping artifact" is semantic, and for edge cases like generated documents and config files you have to think each time. The decision table illustrates by enumeration; it doesn't exhaust the space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the opening question
&lt;/h2&gt;

&lt;p&gt;The further you extend autonomous execution, the more drift from intent becomes visible only after the fact. But put the human back on review and the point of autonomy disappears. This looks like a fork &lt;strong&gt;only while you're counting human involvement by number of stops&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Count by content instead and it stops being a fork. Keep the number of stops the same, and move what they read at the stop from the artifact to the intent. &lt;strong&gt;What you cut isn't human involvement — it's the time a human spends re-checking the correctness of artifacts.&lt;/strong&gt; The machine was always better at that, and when a human does it, volume wins and it becomes a formality.&lt;/p&gt;

&lt;p&gt;To the question of who the automation is for, this is the answer. &lt;strong&gt;Not to go fast, but so that what you intended gets built fast.&lt;/strong&gt; If so, there's only one thing the human should hold to the end. Intent. The decision table was a tool for taking everything else away from them.&lt;/p&gt;

&lt;p&gt;When you're talking about reducing human review, what's left isn't the work you forgot to cut. It's the work you can't cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/claude-harness" rel="noopener noreferrer"&gt;claude-harness&lt;/a&gt; — my public harness, including the conventions file discussed here (&lt;code&gt;rules/common/human-gate.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/agent-knowledge-cycle" rel="noopener noreferrer"&gt;agent-knowledge-cycle&lt;/a&gt; — the theory-side repository on how far to hand things to the machine and where human approval starts. The conventions in this article are one implementation of it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://doi.org/10.5281/zenodo.20578272" rel="noopener noreferrer"&gt;Harness Alignment and Harness Drift&lt;/a&gt; — a paper on where approval gates sit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt; — other repositories&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>aiagents</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Opus 5 Changed How Rules Should Be Written — Audit Yours</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Tue, 28 Jul 2026 11:38:58 +0000</pubDate>
      <link>https://dev.to/shimo4228/opus-5-changed-how-rules-should-be-written-audit-yours-4fb4</link>
      <guid>https://dev.to/shimo4228/opus-5-changed-how-rules-should-be-written-audit-yours-4fb4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: Anthropic's official guidance for the Claude 5 generation (Opus 5 / Fable 5) is "fewer rules, trust the model's judgment." This is a procedure for auditing the custom rules (CLAUDE.md / rules files) you accumulated for older model generations against what the product itself loads, and deciding for each rule: keep, fix, or retire.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've been using Claude Code for a while, you probably share these worries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You wrote CLAUDE.md files and rules to compensate for older models' weaknesses, and you have no idea whether they still help with Claude 5&lt;/li&gt;
&lt;li&gt;The official guidance now says "cut your rules," but you can't tell &lt;strong&gt;which&lt;/strong&gt; of your rules are the ones to cut&lt;/li&gt;
&lt;li&gt;Deleting everything at once and watching behavior degrade is scary, so you don't touch any of it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On 2026-07-25 I ran this audit on my own &lt;code&gt;~/.claude&lt;/code&gt; harness (my set of custom configuration: CLAUDE.md, rules files, agent definitions) and cut the resident rules — the part auto-loaded into every session — from 5,789 down to 2,463 words (measurement details at the end). This article documents the cross-checking procedure I used, in a reproducible form. The takeaway is not the reduction itself but &lt;strong&gt;the decision procedure: on what evidence, which rules, and how to dispose of them&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: the official guidance on rule-writing changed
&lt;/h2&gt;

&lt;p&gt;On 2026-07-24 (the day Opus 5 launched), Anthropic published &lt;a href="https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models" rel="noopener noreferrer"&gt;The new rules of context engineering for Claude 5 generation models&lt;/a&gt;. Two points matter here.&lt;/p&gt;

&lt;p&gt;First, Anthropic itself deleted more than 80% of Claude Code's system prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We removed over 80% of Claude Code's system prompt for models like Claude Opus 5 and Claude Fable 5 with no measurable loss on our coding evaluations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Second, the post spells out the shift in context engineering as six Then → Now pairs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Then (older generations)&lt;/th&gt;
&lt;th&gt;Now (Claude 5 generation)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Give rules&lt;/td&gt;
&lt;td&gt;Trust judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Give examples&lt;/td&gt;
&lt;td&gt;Design interfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Front-load everything&lt;/td&gt;
&lt;td&gt;Progressive disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat yourself&lt;/td&gt;
&lt;td&gt;Simple tool descriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write memory into CLAUDE.md&lt;/td&gt;
&lt;td&gt;Auto-memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sparse specs&lt;/td&gt;
&lt;td&gt;Rich reference material&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The post names a concrete harm of over-instruction: &lt;strong&gt;conflicting instructions&lt;/strong&gt;. System prompt, skills, and user instructions collide, so that "leave documentation as appropriate" and "DO NOT add comments" &lt;strong&gt;coexist in the same request&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, the reason to cut rules is not just token savings. The problem is that &lt;strong&gt;your custom rules conflict with the product's own instructions, and you are forcing the model to resolve the contradiction&lt;/strong&gt;. From here, the procedure is about finding which of your rules are the conflicting ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premise: the ground truth is the system prompt and tool descriptions
&lt;/h2&gt;

&lt;p&gt;What actually reaches the model in the same context as your custom rules is the &lt;strong&gt;system prompt and the tool descriptions&lt;/strong&gt;. That is the ground truth to audit against, and the first step is simply to read it. Official docs and blog posts state recommendations, but they are not loaded at inference time.&lt;/p&gt;

&lt;p&gt;The meaning of a mismatch changes with this distinction, so let's name the two layers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;th&gt;Loaded at inference time?&lt;/th&gt;
&lt;th&gt;Name of the mismatch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;System prompt + tool descriptions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (loaded into context alongside your rules)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Conflict&lt;/strong&gt; (the model is forced to resolve a contradiction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Guidance layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Official docs and blog posts&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Drift&lt;/strong&gt; (you've merely diverged from official recommendations)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A mismatch only qualifies as a conflict when both sides are in the same context (a tool description is only loaded in sessions where that tool is available).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All examples below come from my own &lt;code&gt;~/.claude&lt;/code&gt; harness (Claude Code 2.1.220, audited 2026-07-25 to 07-26).&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: capture the current runtime layer from a live session
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first task is to know exactly what your custom rules are sharing context with. There is a trap here: &lt;strong&gt;the runtime layer is injected from outside your config repository too, so no amount of reading &lt;code&gt;~/.claude&lt;/code&gt; shows you the whole picture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A concrete example. My session's system prompt contained this resident line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Do not call the AgentTool unless the user requested it"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yet grepping &lt;code&gt;~/.claude&lt;/code&gt; finds this string in no file at all (I haven't identified the source; I suspect the harness itself or a plugin, but couldn't confirm). If you only audit config files, you miss this class of instruction.&lt;/p&gt;

&lt;p&gt;So capture from a live session instead. There are two techniques.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For tool descriptions, get the real thing.&lt;/strong&gt; Ask Claude Code to print the target tool's description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Quote the description of the EnterPlanMode tool verbatim. Do not summarize.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For the system prompt, have the session quote itself.&lt;/strong&gt; This is self-reported, so say "verbatim" explicitly to prevent summarizing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;From the system prompt currently loaded, quote verbatim the instructions
about plan mode / commit messages / scope handling.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is to ask per theme. "Print everything" is too long and summaries creep in. Build the theme list &lt;strong&gt;from your own rules&lt;/strong&gt;: open every custom rules file (rules / CLAUDE.md) first, assign each instruction to a theme (planning, commits, review, scope, ...), then query the runtime layer theme by theme. That way you at least eliminate audit gaps on your side of the comparison.&lt;/p&gt;

&lt;p&gt;Two caveats. First, gaps in the other direction — runtime instructions you don't know exist — can't be fully caught by this procedure, so zero detections means "this question found nothing," not "no conflicts." Second, &lt;strong&gt;both capture techniques are model-mediated self-reports, with no guarantee of exactly matching the actual input&lt;/strong&gt;. Treat the captures as a screening pass; before acting on a rule's disposition (retire / invert), confirm the same wording reproduces in a separate session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: cross-check against your rules and sort into 3 categories
&lt;/h2&gt;

&lt;p&gt;Cross-check the captured runtime layer against your custom rules and sort each mismatch into three categories.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Detected in my harness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conflict&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An instruction or claim that contradicts the runtime layer is loaded at the same time&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redundant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The runtime layer already says nearly the same thing&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Diverged from guidance-layer (official docs) recommendations&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Conflict example 1: a no-plan-mode rule
&lt;/h3&gt;

&lt;p&gt;My &lt;code&gt;planning.md&lt;/code&gt; contained this, written as a countermeasure to older generations' habit of not planning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On direct implementation instructions, execute immediately. Do not enter plan mode.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Meanwhile, the &lt;code&gt;EnterPlanMode&lt;/code&gt; tool description loaded in the same context (Claude Code 2.1.220) says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Prefer using EnterPlanMode for implementation tasks unless they're simple."&lt;br&gt;
"If unsure whether to use it, err on the side of planning"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two instructions pointing in exactly opposite directions were loaded into the same context, and the model was silently picking one every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conflict example 2: a rule referencing a ghost setting
&lt;/h3&gt;

&lt;p&gt;The second one was a nastier pattern. Background: by default, Claude Code appends a &lt;code&gt;Co-Authored-By: Claude ...&lt;/code&gt; trailer to commit messages — the attribution that shows Claude as a co-author on GitHub. On whether to emit that attribution, my &lt;code&gt;git-workflow.md&lt;/code&gt; said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Attribution disabled globally via ~/.claude/settings.json.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But checking the actual &lt;code&gt;settings.json&lt;/code&gt;, the relevant key (&lt;code&gt;includeCoAuthoredBy&lt;/code&gt;) &lt;strong&gt;did not exist&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json; print('&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;'.join(sorted(json.load(open('settings.json')).keys())))"&lt;/span&gt;
&lt;span class="c"&gt;# includeCoAuthoredBy is not in the output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether I deleted the setting at some point or the note was wrong from day one, there's no way to tell anymore. What I can say for certain: &lt;strong&gt;a rule cannot detect that its own justification has vanished&lt;/strong&gt;. Code errors out when its referent disappears; a rule keeps asserting "disabled" while sitting in context, contradicting the runtime layer's instruction to append the Co-Authored-By trailer to commit messages.&lt;/p&gt;

&lt;p&gt;Strictly speaking, this line is not an instruction ("don't add the trailer") but a (false) statement of fact ("already disabled"). But since it kept riding along in the same context as information contradicting a runtime-layer instruction, I counted it on the conflict side.&lt;/p&gt;

&lt;p&gt;How the unresolved conflict was actually being resolved (a model-dependent observation, with limits)&lt;/p&gt;

&lt;p&gt;Digging through session logs from the period when this ghost-setting conflict sat unresolved, I found an interesting split. Same Claude Code version (2.1.220), same rules: two Opus 5 sessions &lt;strong&gt;added&lt;/strong&gt; the trailer to commits, one Fable 5 session &lt;strong&gt;did not&lt;/strong&gt;. Within each session, behavior was 100% consistent.&lt;/p&gt;

&lt;p&gt;So when you leave contradictory instructions in place, how they get resolved is up to the model — and when the model changes, the behavior can change too.&lt;/p&gt;

&lt;p&gt;The limits, stated plainly: this is a natural observation, not a controlled comparison (5 commits with differing tasks and user instructions). The most I can claim is "likely model-dependent." Also, the observed difference — attribution — is harmless; whether conflicts with real consequences split the same way is unverified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redundancy example: a scope-adherence rule
&lt;/h3&gt;

&lt;p&gt;Separate from conflicts, some rules had simply become unnecessary to say. My custom rule "when a scope is specified, adhere to it strictly" exists almost verbatim in the Claude 5 generation system prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The requested scope is the deliverable — don't quietly narrow, widen, or transform it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No collision, so the harm is small — but I was paying resident tokens to repeat what the product already says. It was exactly the official post's "Repeat yourself → Simple tool descriptions" shift.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift example: a confidence threshold for review
&lt;/h3&gt;

&lt;p&gt;I detected three drift items. I'll cover one representative case here; the other two — the handling of pre-commit verification steps and the rule splitting review into a separate agent — get different verdicts, so they're covered in Step 3 and the pitfalls section respectively.&lt;/p&gt;

&lt;p&gt;The representative case: my review-agent definition said "only report findings you are at least 80% confident in" (a number unrelated to the 80% system prompt reduction above). The &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5" rel="noopener noreferrer"&gt;official Opus 5 prompting guide&lt;/a&gt; warns against exactly this kind of suppression instruction, using it as an example (it does not mention the number 80% specifically):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your review prompt says "only report high-severity issues" or "be conservative," the model may follow that instruction literally and report less; ask it to report everything and filter in a separate pass instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a load-time conflict, but with the official docs explicitly saying the model may follow this kind of instruction literally and under-report, leaving it in place means dropped findings. I haven't verified how much damage it did on older generations, but since it's the exact pattern the current generation's docs warn against by example, I classified it as drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: don't treat "conflict = bad" — run each item through a decision frame
&lt;/h2&gt;

&lt;p&gt;Once the three-way sort is done, decide dispositions. The important part: &lt;strong&gt;do not mechanically mark conflicts and drift for deletion&lt;/strong&gt;. Some of your custom rules were written precisely to override product defaults on purpose. "Points the opposite way" alone cannot distinguish an accident from a deliberate choice.&lt;/p&gt;

&lt;p&gt;I judged each item on four axes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Did you write it to override the product default, or was it simply not conflicting at the time?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evidence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Is there a record of why it was written — an ADR, an incident note?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Freshness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Does the product behavior it assumed (an old generation's weakness, etc.) still hold?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Expiry condition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Have you defined what would trigger revisiting this rule?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three example dispositions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retire (freshness expired)&lt;/strong&gt;: The no-plan-mode rule (conflict example 1) was a countermeasure to older generations' "start running without planning" weakness. In the Claude 5 generation the product itself now recommends planning; the premise is gone. There was no reason to keep it as an intentional override, so I retired it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retire (conflict + redundancy)&lt;/strong&gt;: Beyond the ghost-setting line (conflict example 2), &lt;code&gt;git-workflow.md&lt;/code&gt; contained nothing but content the model and the runtime layer already cover — commit message format, how to write PRs. Fix the conflicting line and the rest is still redundant. So the whole file had no reason to exist, and I retired it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep (intent and evidence alive)&lt;/strong&gt;: On the other hand, I kept my rule "run review in a separate agent process from the implementer." The official guide contains the line "do not use subagents to verify or double-check your own work," which at first glance collides with this rule.&lt;/p&gt;

&lt;p&gt;But read in context, this is not a rejection of division-of-labor review. That line is one phrase in an &lt;strong&gt;example prompt developers can add to control subagent spawning&lt;/strong&gt;, in a section about cost containment. The official premise is that Opus 5 verifies its own work without being told — explicit verification instructions just cause over-verification and should be cut — and that the new generation, which also delegates more, would otherwise reflexively spawn "just in case" double-check subagents and burn cost. That's the brake being illustrated. The same doc also positively cites the writer–verifier pattern — separating the writer from the verifier — as a strength of Opus 5.&lt;/p&gt;

&lt;p&gt;My rule has two tiers. The base: never let review happen in the same context as the implementation (same-context review inherits the implementer's blind spots). On top of that, for important changes, a &lt;strong&gt;different model&lt;/strong&gt; (in my case, Codex CLI) does the review, decorrelating the blind spots. The latter was an easy keep — cross-model review is a capability Claude Code itself structurally cannot provide no matter how much it evolves, so there is nothing for the product to absorb it into. The design decision is recorded in an ADR.&lt;/p&gt;

&lt;p&gt;The former (separate-process review on the same model) may still overlap with the "added verification passes" the official guidance wants to suppress. This is the interpretive part: if Opus 5's self-verification turns out to solve even the blind-spot-inheritance problem, the base tier may become unnecessary. It stays — as a keep with an expiry condition: revisit when the premise changes.&lt;/p&gt;

&lt;p&gt;Running this decision frame taught me one thing: &lt;strong&gt;rules with recorded evidence (ADRs, incident notes) are fast to judge, and the judgments are more confident&lt;/strong&gt;. Having a record doesn't prevent retirement (the plan-mode rule had a known rationale and was retired anyway, on expired freshness). What the record changes is not the outcome but the cost of deciding. For rules with no recorded rationale, even the question "is it safe to delete?" turns into git log archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall: mechanically applying the official advice misdiagnoses
&lt;/h2&gt;

&lt;p&gt;The official guide says verification steps added by legacy harness scaffolding are also candidates for removal. Applying that mechanically will misdiagnose.&lt;/p&gt;

&lt;p&gt;My custom rules had an 8-item pre-commit Verify step (build / type check / lint / tests / secret scan / dependency audit / doc sync check / git status check). Taken literally, "cut verification steps" makes all eight look like deletion targets.&lt;/p&gt;

&lt;p&gt;But what the official guidance objects to is &lt;strong&gt;instructions that increase the model's self-verification&lt;/strong&gt;. Most of the eight items are deterministic command runs — builds, tests — that add zero model judgment. The axis is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The verification is done by a &lt;strong&gt;machine&lt;/strong&gt; (command, hook) → fine to keep&lt;/li&gt;
&lt;li&gt;The verification is done by the &lt;strong&gt;model, on its own judgment&lt;/strong&gt; → the official guidance's target; consider cutting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other pitfall: &lt;strong&gt;cases where deletion isn't enough and you need an inversion&lt;/strong&gt;. If you simply delete the "only report at ≥80% confidence" line, the section it lived in (a noise-suppression framework) survives and keeps pushing in the suppressive direction. I rewrote the section to: "Do not suppress by confidence. Report everything; the caller filters in a separate pass." An instruction whose correct direction has flipped must be rewritten in the opposite direction, not merely removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results and numbers
&lt;/h2&gt;

&lt;p&gt;The center of gravity is the procedure, but for reference, my harness before and after (measured with &lt;code&gt;wc -w&lt;/code&gt;, resident total of CLAUDE.md + rules):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point in time&lt;/th&gt;
&lt;th&gt;Resident words&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before the audit&lt;/td&gt;
&lt;td&gt;5,789&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After (as of 2026-07-26)&lt;/td&gt;
&lt;td&gt;2,463&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that I am not claiming "resolving the conflicts improved performance." My environment has no instrument for measuring the cost of contradiction-resolution; the quantitative case rests on Anthropic's measurement quoted at the top (80% deletion, no degradation). What this article demonstrates is not the effect but &lt;strong&gt;real examples of config drift, and a procedure for finding and disposing of it&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: what you can do next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Capture the runtime layer (system prompt + tool descriptions) from a live session. Reading your config repository alone doesn't show the whole picture&lt;/li&gt;
&lt;li&gt;Cross-check against your custom rules; sort into conflict / redundant / drift&lt;/li&gt;
&lt;li&gt;Don't treat "conflict = bad" — judge keep / invert / retire on the four axes of intent, evidence, freshness, and expiry condition&lt;/li&gt;
&lt;li&gt;Apply the official reduction advice with the distinction between &lt;strong&gt;model self-verification&lt;/strong&gt; and &lt;strong&gt;machine verification&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one lesson to add from this round: &lt;strong&gt;when you write a rule, record its rationale and its expiry condition&lt;/strong&gt;. I'll confess I wasn't doing this systematically either. Rules with an ADR (design decision record) took minutes to judge; rules without one turned into git log archaeology; and for the ghost setting, I couldn't even pin down when it disappeared. A rule cannot detect that its own justification has vanished — so whether you'll be able to judge your rules at the next generation change will be decided by the notes you leave now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postscript (2026-07-26)
&lt;/h2&gt;

&lt;p&gt;The lessons from this article have already flowed back into my harness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every resident rule file now carries &lt;code&gt;rationale:&lt;/code&gt; and &lt;code&gt;review-when:&lt;/code&gt; metadata, and my rules-audit skill &lt;a href="https://github.com/shimo4228/rules-stocktake" rel="noopener noreferrer"&gt;rules-stocktake&lt;/a&gt; reads these fields during audits&lt;/li&gt;
&lt;li&gt;The procedure in this article (collect the runtime layer → cross-check → judge with the decision frame) has been generalized into a skill called &lt;a href="https://github.com/shimo4228/generation-audit" rel="noopener noreferrer"&gt;generation-audit&lt;/a&gt;, published as a standalone repo, so it can be re-run as-is at the next model generation change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models" rel="noopener noreferrer"&gt;The new rules of context engineering for Claude 5 generation models&lt;/a&gt; — Anthropic official blog (2026-07-24)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5" rel="noopener noreferrer"&gt;Prompting Claude Opus 5 — Claude Platform Docs&lt;/a&gt; — official recommendations on confidence thresholds, verification steps, and subagents&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/generation-audit" rel="noopener noreferrer"&gt;generation-audit&lt;/a&gt; — this article's procedure packaged as a skill (standalone repo)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/rules-stocktake" rel="noopener noreferrer"&gt;rules-stocktake&lt;/a&gt; — resident-rules audit skill (now reads rationale / review-when metadata)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt; — the author's GitHub (harness-related repositories)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>contextengineering</category>
      <category>promptengineering</category>
      <category>anthropic</category>
    </item>
    <item>
      <title>claude-security Beta: Cost, Output, Accuracy — 189 Agents, 2 Hours, Measured</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:00:04 +0000</pubDate>
      <link>https://dev.to/shimo4228/claude-security-beta-cost-output-accuracy-189-agents-2-hours-measured-1eji</link>
      <guid>https://dev.to/shimo4228/claude-security-beta-cost-output-accuracy-189-agents-2-hours-measured-1eji</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: A measured run of Anthropic's official security-scanning plugin &lt;code&gt;claude-security&lt;/code&gt; (beta). What the tool does, how long it takes, what output it returns, and how far you can trust it — backed by the raw data from the run's own artifacts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You may know the official plugin exists and still not have tried it, for reasons like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cost is unreadable.&lt;/strong&gt; It warns you at startup that it "may take a while and use a significant number of tokens," but never says how much&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output quality is unknown.&lt;/strong&gt; How is this different from existing static analysis, and does an LLM that only reads code produce findings you can act on?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's unclear what to do afterward.&lt;/strong&gt; If dozens of findings come back, where do you start?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On 2026-07-25 I installed v0.10.0 and scanned my entire &lt;code&gt;~/.claude&lt;/code&gt; configuration set (hooks, skills, agents, permission settings). &lt;strong&gt;189 subagents ran for two hours and returned 20 findings.&lt;/strong&gt; This article is that measurement.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This plugin is in beta as of 2026-07 (the version number is 0.10.0). Every number and behavior below is measured on v0.10.0 and may change.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Measured on 2.1.220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude-security&lt;/code&gt; v0.10.0 (by Anthropic, marketplace &lt;code&gt;claude-plugins-official&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;python3 3.9+ (used by the report-generation scripts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git&lt;/td&gt;
&lt;td&gt;Change scans require a git checkout. Whole-repository scans work without git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission mode&lt;/td&gt;
&lt;td&gt;auto mode recommended (the plugin says so itself)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This plugin is the "runs inside your session" version of the hosted &lt;a href="https://claude.com/product/claude-security" rel="noopener noreferrer"&gt;Claude Security&lt;/a&gt; product. It starts no separate process and no daemon; it runs entirely inside your Claude Code session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool is, and what happens when you run it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  From install to launch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run these inside a Claude Code session&lt;/span&gt;
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;claude-security@claude-plugins-official
/reload-plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it reports that the marketplace was not found, run &lt;code&gt;/plugin marketplace add anthropics/claude-plugins-official&lt;/code&gt; first and retry.&lt;/p&gt;

&lt;p&gt;After installing, &lt;code&gt;/claude-security&lt;/code&gt; opens a menu offering three jobs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scan codebase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The whole repository, or a scoped part of it&lt;/td&gt;
&lt;td&gt;Depends on size (2 hours 1 minute in my run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scan changes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A branch diff, a pull request diff, or a single commit&lt;/td&gt;
&lt;td&gt;Minutes for a small diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Suggest patches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Turns an existing report's findings into patch files&lt;/td&gt;
&lt;td&gt;Depends on the finding count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also skip the menu and name the job in the arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/claude-security scan my branch's changes
/claude-security --base main
/claude-security 3cb30d2          # a hex string of 7+ chars is read as a commit SHA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get the confirmation prompt out of the way
&lt;/h3&gt;

&lt;p&gt;Choosing a whole-repository scan always triggers this confirmation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This scan may take a while and may use a significant number of tokens. You will need to leave Claude Code open while the scan completes. Are you sure you want to continue?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Accept the cost up front and you go straight in. The check is not a literal string match — it asks whether your request already reads as accepting the time and token cost — so include a sentence that conveys that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/claude-security scan this repository — whole codebase.
I understand it may take a while and use a significant number of tokens.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You do need to leave Claude Code open while the scan runs. No interaction is required, so you can walk away.&lt;/p&gt;
&lt;h3&gt;
  
  
  Effort changes how much it searches, not how hard it verifies
&lt;/h3&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;--effort&lt;/code&gt; has four tiers. The distinctive design choice here: &lt;strong&gt;the verification panel is fixed at three voters at every tier.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;effort&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One researcher over the whole repository. No inventory, threat model, or breadth sweep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;medium&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;The full workflow: inventory → threat model → one researcher per component × category → one sweep → three-voter panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;medium&lt;/code&gt;, but the component cap rises to 24, two researchers per cell, two sweeps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;high&lt;/code&gt;, plus an adversarial phase: marginal keeps are re-panelled and every survivor faces a red team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So dropping the tier does not mean "wave things through" — it means "search a narrower area." The documentation says as much, noting that the report's confidence figures are calibrated against that three-voter panel. I ran &lt;code&gt;medium&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A six-stage pipeline runs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A["Inventory&amp;lt;br/&amp;gt;partition the tree"] --&amp;gt; B["Threat model&amp;lt;br/&amp;gt;one per component"]
    B --&amp;gt; C["Research&amp;lt;br/&amp;gt;component × category"]
    C --&amp;gt; D["Sweep&amp;lt;br/&amp;gt;what the matrix missed"]
    D --&amp;gt; E["Panel&amp;lt;br/&amp;gt;3-lens adversarial check"]
    E -. max only .-&amp;gt; F["Adversarial&amp;lt;br/&amp;gt;repanel + red team"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what that came to against &lt;code&gt;~/.claude&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Agents&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inventory&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Partitioned the tree into 11 components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threat model&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;One per component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research + Sweep&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;One per component × category cell; the sweep is counted in this same pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Panel&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;td&gt;45 candidates × 3 votes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;189&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 hours 1 minute&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This breakdown and the elapsed time are measured, not estimated. You can reproduce the same tally on your own run: the workflow's &lt;code&gt;journal.jsonl&lt;/code&gt; records each agent's return value, so you can classify agents by the shape of what they returned.&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;# Run inside ~/.claude/projects/&amp;lt;project&amp;gt;/&amp;lt;session-id&amp;gt;/subagents/workflows/wf_*/&lt;/span&gt;
python3 - &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;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json, collections
sig = collections.Counter()
with open("journal.jsonl", encoding="utf-8", errors="replace") as f:
    for line in f:
        try: d = json.loads(line)
        except ValueError: continue
        if d.get("type") != "result": continue
        r = d.get("result")
        sig[tuple(sorted(r))[:5] if isinstance(r, dict) else ("?",)] += 1
for k, v in sig.most_common(): print(v, k)
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;135 ('reasoning', 'verdict')                                          &amp;lt;- panel votes
 42 ('findings',)                                                     &amp;lt;- researchers
 11 ('assumptions', 'entryPoints', 'hotFiles', 'sinks', 'trustBoundaries')  &amp;lt;- threat models
  1 ('components', 'securityScanSkippedComponents')                    &amp;lt;- inventory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The elapsed time comes from the same logs. The first record is &lt;code&gt;02:28:12Z&lt;/code&gt; and the last is &lt;code&gt;04:29:16Z&lt;/code&gt;. The largest gap in between is 47 seconds, so something is running for the entire two hours.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;this does not mean you are tied up for two hours.&lt;/strong&gt; You must leave the Claude Code window open; you do not have to do anything. The plugin's internal documentation has a line that assumes exactly this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Users desire to leave the session unattended very soon after kicking off a scan, around a minute of wall-clock time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That matched my experience — I spent the interval in a separate session doing unrelated work.&lt;/p&gt;

&lt;p&gt;Progress is visible stage by stage in &lt;code&gt;/workflows&lt;/code&gt; while it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looked like
&lt;/h2&gt;

&lt;p&gt;When it finishes, three artifacts land in a timestamped directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLAUDE-SECURITY-20260725-022756/
├── CLAUDE-SECURITY-RESULTS.md        # the human-readable report
├── CLAUDE-SECURITY-RESULTS.jsonl     # machine-readable (one finding per line)
└── CLAUDE-SECURITY-REVISION-*.json   # the revision and settings stamp for the run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was &lt;strong&gt;20 findings (5 HIGH / 15 MEDIUM)&lt;/strong&gt;. 111 raw candidates became 83 after dedup; of the 45 that reached the panel, 25 were rejected, leaving these 20.&lt;/p&gt;

&lt;p&gt;A rejection rate above half can be read as evidence that the panel is working, or as evidence that the research stage over-generates candidates and the panel cleans up after it. The report contains nothing that decides between those readings.&lt;/p&gt;

&lt;p&gt;What does decide something is the number on the way out. &lt;strong&gt;I acted on all 20 findings, and rejected none as a false positive.&lt;/strong&gt; I confirmed each one reproduced before fixing it (three were addressed only partially — I did not add OS-level isolation). This is one repository and one run, but at least in this case, a tool that only reads and reasons did not produce a pile of off-target noise.&lt;/p&gt;

&lt;p&gt;Three things about the output quality stood out.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It reads configuration and natural-language instruction files
&lt;/h3&gt;

&lt;p&gt;The inventory partitioned the tree like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hooks / scripts / scheduled-tasks / skills-executable-scripts /
skills-instructions / agents / rules / templates / docs /
notes-and-metrics / tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rules&lt;/code&gt; and &lt;code&gt;skills-instructions&lt;/code&gt; — meaning &lt;strong&gt;the instruction files written in plain natural language were treated as their own audit components.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 20 findings broke down by location like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hooks: 10 / skills: 5 / settings.json: 3 / scheduled-tasks: 1 / agents: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one under &lt;code&gt;agents&lt;/code&gt; was about &lt;strong&gt;the trust-ordering instruction itself&lt;/strong&gt;, written in an agent definition &lt;code&gt;.md&lt;/code&gt;. One of my agents treated raw session logs as "machine records, hard to alter" and placed them at the top of its trust order.&lt;/p&gt;

&lt;p&gt;But those logs hold the verbatim bodies of external pages fetched in the past. The file may be hard to tamper with; its contents are not trustworthy.&lt;/p&gt;

&lt;p&gt;The one under &lt;code&gt;scheduled-tasks&lt;/code&gt; was similar in character: a README said a job was "not wired up," when it was in fact registered and running weekly. &lt;strong&gt;It flags documentation that contradicts reality as something that makes reviewers misjudge risk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tool that searches for patterns with regular expressions produces neither of these.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It folds the count down instead of inflating it
&lt;/h3&gt;

&lt;p&gt;The report opens with this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read the count with one caveat: the 20 are not 20 distinct defects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It then names three clusters itself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cluster&lt;/th&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;th&gt;What it actually is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Permission allowlist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The same line of &lt;code&gt;settings.json&lt;/code&gt; seen through three lenses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Log-guard leaks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Three hooks protecting the same asset, all failing open because of one shared constant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Child-agent execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Two paths where a skill launching a child Claude executed generated strings directly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those three clusters account for 15; the remaining 5 stood alone. The report describes the whole as "closer to eight problems" when counted by root cause.&lt;/p&gt;

&lt;p&gt;It goes further and &lt;strong&gt;names a single repair as the highest-leverage one in the eight-finding cluster&lt;/strong&gt; — fix that one spot and seven close at once. In a field where finding count tends to be treated as a performance metric, the report folds its own count down for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The machine-readable side lets you sort by what you care about
&lt;/h3&gt;

&lt;p&gt;The report body is long, so start with the jsonl for the shape of it.&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;# Run in the parent directory of CLAUDE-SECURITY-&amp;lt;timestamp&amp;gt;/&lt;/span&gt;
python3 - &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;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json, collections, glob
path = glob.glob("CLAUDE-SECURITY-*/CLAUDE-SECURITY-RESULTS.jsonl")[0]
with open(path) as f:
    rows = [json.loads(line) for line in f]
print("count:", len(rows), collections.Counter(r["severity"] for r in rows))
print("where:", collections.Counter(r["file"].split("/")[0] for r in rows))
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;count: 20 Counter({'MEDIUM': 15, 'HIGH': 5})
where: Counter({'hooks': 10, 'skills': 5, 'settings.json': 3, 'scheduled-tasks': 1, 'agents': 1})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line carries fields including &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;severity&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;exploit_scenario&lt;/code&gt;, &lt;code&gt;preconditions&lt;/code&gt;, &lt;code&gt;recommendation&lt;/code&gt;, and &lt;code&gt;cwe_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;confidence&lt;/code&gt; is worth reading alongside &lt;code&gt;severity&lt;/code&gt;, because that value is set by the panel's vote.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Confidence in this report is clamped by that vote — only unanimous panels claim &lt;code&gt;high&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How far you can trust it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The tool discloses its own limits.&lt;/strong&gt; Three things that static-analysis reports tend to omit were stated outright.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limits the report writes down itself
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Some candidates go unreviewed.&lt;/strong&gt; In my run, 38 candidate sites never reached the panel after dedup and capping. The earlier "45 candidates × 3 votes" counts only what reached the panel; these 38 are not in it. The report states plainly that absence from the report is not evidence of absence in the tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It names what it excluded.&lt;/strong&gt; In my run, three vendored Python virtualenvs and &lt;code&gt;__pycache__&lt;/code&gt; — with the note that if a dependency inside those virtualenvs is itself compromised, this scan did not see it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It accounts for coverage.&lt;/strong&gt; Every top-level directory must be either scanned or set aside with a stated reason, and that reconciliation runs &lt;strong&gt;before&lt;/strong&gt; the search begins. All 11 of my directories were accounted for&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  It executes no code
&lt;/h3&gt;

&lt;p&gt;No tests run, no exploit fires, no proof of concept is validated. Everything is derived from reading. The heaviest finding in my run — an arbitrary code execution path — was confirmed not by planting an attack file but by &lt;strong&gt;reading it against a similar neighboring hook&lt;/strong&gt; (which turned out to already carry the same defense).&lt;/p&gt;

&lt;p&gt;That is the safe design, but the flip side is that &lt;strong&gt;reproducible confirmation becomes your job.&lt;/strong&gt; I wrote a failing test first for each fix before implementing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anthropic states that it is nondeterministic
&lt;/h3&gt;

&lt;p&gt;From the README:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scans are nondeterministic. Two scans of the same code can surface different findings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It goes on to say that because it reasons about code the way a human security researcher does, it &lt;strong&gt;complements rather than replaces&lt;/strong&gt; SAST, dependency scanning, and code review. This is not a drop-in substitute for static analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  It writes nothing
&lt;/h3&gt;

&lt;p&gt;A scan only reads. Even the patch-generation job leaves your working tree untouched: nothing is committed, pushed, or opened as a pull request. Patch files land on disk and you decide whether to apply them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do afterward
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How to order the fixes
&lt;/h3&gt;

&lt;p&gt;I ranked them in four tiers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Independently sufficient for arbitrary code execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;It bypasses every other defense, so fixing anything else while this stands is pointless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A cluster that reduces to one constant or anchor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One fix closes several findings at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A definition with the trust direction inverted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The code may be correct while the premise of the judgment is broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Documentation that contradicts reality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The harm is indirect, but it gives reviewers false reassurance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Priority 2 is handed to you by the report, so &lt;strong&gt;look for wording to the effect of "highest-leverage" in the body&lt;/strong&gt; before planning anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  You do not have to take the recommendation as given
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;recommendation&lt;/code&gt; field is accurate, but it is not the only answer. One example.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;settings.json&lt;/code&gt; carried entries like &lt;code&gt;Bash(bash:*)&lt;/code&gt; and &lt;code&gt;Bash(python:*)&lt;/code&gt; that grant a whole interpreter. With those present, the other ~80 narrowly scoped grants are moot, because &lt;code&gt;bash -c '&amp;lt;anything&amp;gt;'&lt;/code&gt; matches by prefix.&lt;/p&gt;

&lt;p&gt;The report recommended moving these into &lt;code&gt;permissions.deny&lt;/code&gt;. I chose &lt;strong&gt;removal from the allowlist&lt;/strong&gt; instead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;deny is a refusal, so it makes legitimate execution permanently impossible&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dropping an entry from the allowlist merely returns it to a confirmation prompt in auto mode, which hands the judgment back to a human&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The replacement shape is a shift from "grant by interpreter name" to "grant by the script you actually run."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-      "Bash(python:*)",
-      "Bash(python3:*)",
-      "Bash(sh:*)",
-      "Bash(bash:*)",
-      "Bash(node:*)",
-      (export / source / command removed too — 8 entries in all)
+      "Bash(bash ~/.claude/hooks/:*)",
+      "Bash(bash ~/.claude/scripts/:*)",
+      "Bash(bash ~/.claude/tests/:*)",
+      "Bash(python3 ~/.claude/skills/:*)",
+      "Bash(python3 -m pytest:*)",
+      "Bash(python3 -m scripts.:*)",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The finding is right; the recommendation is one option&lt;/strong&gt; — that is the distance to keep.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day to day, put it on Scan changes
&lt;/h3&gt;

&lt;p&gt;Running the whole-repository scan every time is not realistic. For routine use, &lt;strong&gt;Scan changes&lt;/strong&gt; is the one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It targets a branch diff, a pull request diff, or a single commit&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;medium&lt;/code&gt;, a diff of at most 5 files and 300 changed lines runs the lighter single-researcher shape instead of the full component matrix (the panel verification is unchanged)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only committed changes are in scope.&lt;/strong&gt; Uncommitted work in the tree is not part of any diff, so commit or stash first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the patch-generation job, &lt;strong&gt;scan with a clean working tree.&lt;/strong&gt; When the run's stamp carries &lt;code&gt;revision.dirty: true&lt;/code&gt;, the job stops before drafting anything. Mine ran dirty, so that path was unavailable.&lt;/p&gt;

&lt;p&gt;The scanner tripped one of my own guardrails&lt;/p&gt;

&lt;p&gt;My setup includes a homegrown mechanism that detects attempts to abuse the auto-mode confirmation skip. During the scan, one panel voter systematically searched the Claude Code binary for permission-bypass symbols and set it off.&lt;/p&gt;

&lt;p&gt;That candidate was rejected 0-3 by the panel, but the report records the behavior rather than burying it. The original phrasing was precise about why — a scanner probing its own guardrails is exactly the thing a reader should be told about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As a measurement of the beta, here is what to take away.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; — one configuration directory (11 components) at &lt;code&gt;medium&lt;/code&gt; came to 189 subagents over 2 hours. Put routine use on Scan changes instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Character of the output&lt;/strong&gt; — it produces findings regular expressions do not. Configuration and natural-language instruction files enter the audit scope, and documentation that contradicts reality gets flagged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precision, anecdotally&lt;/strong&gt; — of the 20 findings delivered, I rejected none as a false positive. One repository and one run, but this is not a tool built on the assumption that you skim and discard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to read the count&lt;/strong&gt; — it does not inflate the number; it folds the findings into clusters and names the highest-leverage repair. Look there first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounds of trust&lt;/strong&gt; — unreviewed candidate counts, excluded areas, code never executed, nondeterminism: the report discloses all of it. A complement to static analysis, not a replacement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be straight about it: &lt;strong&gt;I have not re-scanned after the fixes.&lt;/strong&gt; I have not asked the tool itself to confirm that the findings are closed, and since it is nondeterministic there is no guarantee the same result would come back anyway. Taking that into account, treating it as "one more reviewer on the team" seems like the right weight for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-plugins-official" rel="noopener noreferrer"&gt;anthropics/claude-plugins-official&lt;/a&gt; — where the &lt;code&gt;claude-security&lt;/code&gt; plugin is distributed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.com/product/claude-security" rel="noopener noreferrer"&gt;Claude Security&lt;/a&gt; — the hosted version of the same capability (this article covers the in-session plugin)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt; — my repositories&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>anthropic</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>herdr, a tmux for AI Agents — Until the Editor Disappeared</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:00:06 +0000</pubDate>
      <link>https://dev.to/shimo4228/herdr-a-tmux-for-ai-agents-until-the-editor-disappeared-3hnn</link>
      <guid>https://dev.to/shimo4228/herdr-a-tmux-for-ai-agents-until-the-editor-disappeared-3hnn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: how to build a terminal environment where you can monitor multiple Claude Code sessions with live status, come back to the same sessions after stepping away or over SSH, and — the interesting part — &lt;strong&gt;let the agents reorganize their own screen layout&lt;/strong&gt;. It's a field log from installation to the point where the editor's (Zed's) role shrank from "writing" to "sign-off" (reviewing and approving) to "gone."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Intro — the "where do I put them" problem of parallel agents
&lt;/h2&gt;

&lt;p&gt;Once you start running multiple Claude Code or Codex sessions in parallel, you hit these walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terminal tabs pile up, and remembering "which tab was doing what" costs you time every single round&lt;/li&gt;
&lt;li&gt;Quitting your editor or terminal app kills the agent sessions running inside it&lt;/li&gt;
&lt;li&gt;You can't check on your running agents from your iPhone or another machine while away from your desk&lt;/li&gt;
&lt;li&gt;Agents can't touch their own execution environment (pane layout, workspaces) at all. Rearranging things is always manual human work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article solves these with &lt;strong&gt;herdr&lt;/strong&gt; (an agent multiplexer — a terminal multiplexer built for agents). I'm a Zed user, and right after installing it my verdict was "isn't this redundant with what Zed already does?" — until &lt;strong&gt;the moment I let an agent manipulate the layout itself, which flipped my evaluation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And the flip didn't stop there. Once herdr became my main battleground, the editor's (Zed's) role shrank from "a tool for writing" to "a tool for signing off on agent output" — and eventually it lost even that role and I stopped opening it. This is the whole story, from installation to the editor becoming unnecessary, with the configs and commands that actually worked.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;macOS + Homebrew (herdr also runs on Linux)&lt;/li&gt;
&lt;li&gt;You're already using CLI agents such as Claude Code&lt;/li&gt;
&lt;li&gt;This article is based on &lt;strong&gt;herdr v0.7.4&lt;/strong&gt; (verified 2026-07-18). The tool is only about three and a half months old, so the command surface may change&lt;/li&gt;
&lt;li&gt;The editor is Zed (a recent build with the &lt;code&gt;markdown_preview_*&lt;/code&gt; settings), and the terminal I end up choosing is &lt;a href="https://ghostty.org/" rel="noopener noreferrer"&gt;Ghostty&lt;/a&gt; (the how and why comes later)&lt;/li&gt;
&lt;li&gt;Pane IDs in this article (&lt;code&gt;w5:p8&lt;/code&gt; etc.) are real values from my environment. Yours will differ&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What herdr is — only two real differences from tmux
&lt;/h2&gt;

&lt;p&gt;herdr is an "agent multiplexer that lives in your terminal." It's a single Rust binary, and like tmux it persists sessions in a server process (the prefix key is even tmux-compatible: &lt;code&gt;ctrl+b&lt;/code&gt;). GitHub is &lt;a href="https://github.com/ogulcancelik/herdr" rel="noopener noreferrer"&gt;ogulcancelik/herdr&lt;/a&gt;, dual-licensed AGPL-3.0-or-later + commercial (stated in the LICENSE file).&lt;/p&gt;

&lt;p&gt;The essential differences from tmux come down to two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Semantic agent state tracking&lt;/strong&gt; — it auto-detects agents inside panes and lists their state — &lt;code&gt;working&lt;/code&gt; / &lt;code&gt;blocked&lt;/code&gt; / &lt;code&gt;done&lt;/code&gt; / &lt;code&gt;idle&lt;/code&gt; / &lt;code&gt;unknown&lt;/code&gt; — in a sidebar. You can see "which one is waiting on me" without visually patrolling every pane&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A socket API&lt;/strong&gt; — pane splitting, command execution, output reading, and layout changes are all controllable from external processes (CLI commands like &lt;code&gt;herdr pane run&lt;/code&gt; are wrappers around a Unix-socket API). Which means &lt;strong&gt;an agent can operate its own execution environment&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The screen has three levels: &lt;strong&gt;workspace → tab → pane&lt;/strong&gt;. From the CLI you address them as &lt;code&gt;w1&lt;/code&gt; (workspace 1), &lt;code&gt;w1:t1&lt;/code&gt; (tab 1 inside it), and &lt;code&gt;w1:p1&lt;/code&gt; (pane 1). Read the command examples below with this notation in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing — one brew command
&lt;/h2&gt;

&lt;p&gt;It's bottled in homebrew-core, so brew it is. I skipped the official site's &lt;code&gt;curl | sh&lt;/code&gt; because piped install scripts are hard to audit. Including config generation, it's four lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;herdr                                    &lt;span class="c"&gt;# v0.7.4&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/herdr
herdr &lt;span class="nt"&gt;--default-config&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/herdr/config.toml  &lt;span class="c"&gt;# 305-line baseline config&lt;/span&gt;
herdr config check                                    &lt;span class="c"&gt;# → config: ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;herdr&lt;/code&gt; starts the TUI, and the server (&lt;code&gt;herdr server&lt;/code&gt;) comes up automatically. No login-item daemon (&lt;code&gt;brew services&lt;/code&gt;) was needed.&lt;/p&gt;

&lt;p&gt;As a sanity check, run a smoke test of the socket API. This doubles as the minimal example of "letting an agent operate the environment" covered later:&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;# Create a workspace, run a command in a pane, wait for output, read it back&lt;/span&gt;
herdr workspace create &lt;span class="nt"&gt;--cwd&lt;/span&gt; ~ &lt;span class="nt"&gt;--label&lt;/span&gt; smoke-test &lt;span class="nt"&gt;--no-focus&lt;/span&gt;
&lt;span class="c"&gt;# → Returns workspace_id and pane_id as JSON. The lines below assume it returned&lt;/span&gt;
&lt;span class="c"&gt;#   "w1:p1" — substitute whatever IDs you actually got (if you already have&lt;/span&gt;
&lt;span class="c"&gt;#   workspaces, you'll get w2 or later)&lt;/span&gt;
herdr pane run &lt;span class="s2"&gt;"w1:p1"&lt;/span&gt; &lt;span class="s2"&gt;"echo herdr-smoke-ok"&lt;/span&gt;
herdr &lt;span class="nb"&gt;wait &lt;/span&gt;output &lt;span class="s2"&gt;"w1:p1"&lt;/span&gt; &lt;span class="nt"&gt;--match&lt;/span&gt; &lt;span class="s2"&gt;"herdr-smoke-ok"&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 5000
herdr pane &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="s2"&gt;"w1:p1"&lt;/span&gt; &lt;span class="nt"&gt;--lines&lt;/span&gt; 10
&lt;span class="c"&gt;# → If the output contains herdr-smoke-ok, you're wired up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all four pass, layout, execution, and reading are controllable from an external process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Zed is enough, and where it isn't
&lt;/h2&gt;

&lt;p&gt;Let me be honest here: right after installing, my evaluation was "&lt;strong&gt;this is barely different from Zed&lt;/strong&gt;."&lt;/p&gt;

&lt;p&gt;Zed announced &lt;a href="https://zed.dev/blog/parallel-agents" rel="noopener noreferrer"&gt;Parallel Agents&lt;/a&gt; on 2026-04-22. The Threads sidebar runs multiple agents in parallel, with per-thread git worktree isolation. Its terminal already does tabs (&lt;code&gt;cmd+N&lt;/code&gt;) and splits (&lt;code&gt;cmd+D&lt;/code&gt;). If you're at your desk reviewing in a GUI while agents run, Zed's experience is better. herdr's workspace switching looked about the same as switching Zed windows.&lt;/p&gt;

&lt;p&gt;In fact, I almost shelved it with a tidy division of "at the desk = Zed, away = herdr."&lt;/p&gt;

&lt;p&gt;What changed my evaluation was using the layers that &lt;strong&gt;structurally don't exist in Zed&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Zed&lt;/th&gt;
&lt;th&gt;herdr&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GUI review experience&lt;/td&gt;
&lt;td&gt;◎ Parallel Agents + editor integration&lt;/td&gt;
&lt;td&gt;— (terminal only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution persistence&lt;/td&gt;
&lt;td&gt;Quitting the app stops execution (thread history survives)&lt;/td&gt;
&lt;td&gt;Server-resident. Execution continues with every app closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External reattach to a running session&lt;/td&gt;
&lt;td&gt;Not possible (SSH remote dev exists, but it's a different thing)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ssh → herdr&lt;/code&gt; from an iPhone etc. drops you back into the same screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agents operating their own environment&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Full control via the socket API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is Zed's win; the bottom three are territory only herdr has (and the first row's verdict changes later, once plugins enter the picture). So it's not competition — it's a complement: &lt;strong&gt;herdr fills the "managing many agents" layer where Zed is weak&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One more trap that matters in practice: &lt;strong&gt;sessions launched from Zed's agent panel can't be grabbed from outside — not by the official Claude Code app (Remote Control), not by Termius&lt;/strong&gt; (verified myself). They're sealed inside the editor process. A Claude Code launched as a CLI from a terminal shows up in the official app's list and is reachable via SSH → herdr. If there's any chance you'll want to look at a session from outside, launch it as a CLI.&lt;/p&gt;

&lt;p&gt;The next section is the clincher for the complement story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting the agent operate the layout itself
&lt;/h2&gt;

&lt;p&gt;After installing, I wanted my agents — scattered across tabs — on one screen, and on a whim asked Claude Code itself: "consolidate the tabs into one."&lt;/p&gt;

&lt;p&gt;Claude Code executed these three commands:&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;# Move panes from other tabs into tab t2 as splits (executed by Claude Code itself)&lt;/span&gt;
herdr pane move w5:p5 &lt;span class="nt"&gt;--tab&lt;/span&gt; w5:t2 &lt;span class="nt"&gt;--split&lt;/span&gt; right &lt;span class="nt"&gt;--no-focus&lt;/span&gt;
herdr pane move w5:p7 &lt;span class="nt"&gt;--tab&lt;/span&gt; w5:t2 &lt;span class="nt"&gt;--split&lt;/span&gt; down &lt;span class="nt"&gt;--target-pane&lt;/span&gt; w5:p4 &lt;span class="nt"&gt;--no-focus&lt;/span&gt;
herdr pane move w5:p6 &lt;span class="nt"&gt;--tab&lt;/span&gt; w5:t2 &lt;span class="nt"&gt;--split&lt;/span&gt; down &lt;span class="nt"&gt;--target-pane&lt;/span&gt; w5:p5 &lt;span class="nt"&gt;--no-focus&lt;/span&gt;
&lt;span class="c"&gt;# → Panes scattered across tabs become a 2×2 grid on one tab. Emptied tabs auto-close&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two agents were in &lt;code&gt;working&lt;/code&gt; state at the time, and the layout changed &lt;strong&gt;without stopping a single one&lt;/strong&gt;. All I did was say one sentence. No worrying about reopening panes and interrupting processes, no assembling the sequence of move commands by hand.&lt;/p&gt;

&lt;p&gt;Here's what it looked like. Starting from one-tab-per-agent (Before):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs5j5tmzndv7vr6dflkil.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs5j5tmzndv7vr6dflkil.png" alt="Before: one tab per agent. Four tabs in the tab bar, only one session visible at a time" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before: four tabs in the bar. Only one session visible at a time&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I ask the Claude Code in another pane to "consolidate them":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbfan3a4ukb0hweflkn01.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbfan3a4ukb0hweflkn01.png" alt="Instructing Claude Code to consolidate tabs. The screen shows it surveying the tab list and the execution log" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The instruction is a single sentence. Claude Code surveys the current tab structure via the socket API, assembles the pane moves, and runs them&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And everything lands in a 2×2 grid on one tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6onbiow10ndcb2p2r4m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6onbiow10ndcb2p2r4m.png" alt="After: consolidated into a 2×2 grid on a single tab, four panes visible simultaneously" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After: every agent on one screen. Running processes never stopped&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Agents know where they are through environment variables:&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;$ &lt;/span&gt;&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; herdr    &lt;span class="c"&gt;# home directory in the output replaced with ~&lt;/span&gt;
&lt;span class="nv"&gt;HERDR_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;HERDR_PANE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;w5:p8
&lt;span class="nv"&gt;HERDR_SOCKET_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.config/herdr/herdr.sock
&lt;span class="nv"&gt;HERDR_TAB_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;w5:t2
&lt;span class="nv"&gt;HERDR_WORKSPACE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;w5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So an agent knows "which pane am I in," and from there it can split a pane next door, run a command, and read the result. &lt;strong&gt;The shape of the execution environment itself becomes one of the agent's tools.&lt;/strong&gt; Incidentally, the session writing this very article is a Claude Code inside a herdr pane — &lt;code&gt;herdr agent list&lt;/code&gt; shows it as &lt;code&gt;working&lt;/code&gt; on this draft.&lt;/p&gt;

&lt;p&gt;git worktree integration was also a one-liner (verified myself):&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;# Create a worktree + branch + new workspace in one shot&lt;/span&gt;
herdr worktree create &lt;span class="nt"&gt;--workspace&lt;/span&gt; w6 &lt;span class="nt"&gt;--branch&lt;/span&gt; feature-x
&lt;span class="c"&gt;# → Creates a worktree at ~/.herdr/worktrees/&amp;lt;repo-name&amp;gt;/feature-x/ and&lt;/span&gt;
&lt;span class="c"&gt;#    opens a workspace labeled "feature-x" with that as its cwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same "one room per branch, work in parallel" structure as Zed Parallel Agents — except an agent can assemble it from the CLI.&lt;/p&gt;

&lt;p&gt;Gotcha: a zoomed tab refuses layout changes&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pane move&lt;/code&gt; can return &lt;code&gt;changed: false&lt;/code&gt; (reason: &lt;code&gt;"zoomed_tab"&lt;/code&gt;) and do nothing. If the target tab is in zoomed view, layout changes are rejected by design. Run &lt;code&gt;herdr pane zoom  --off&lt;/code&gt; to unzoom first.&lt;/p&gt;

&lt;p&gt;Gotcha: an omitted target means the focused pane, not the caller&lt;/p&gt;

&lt;p&gt;When I had Claude Code run &lt;code&gt;herdr pane split&lt;/code&gt;, the pane split in &lt;strong&gt;a different workspace — the one I happened to be looking at&lt;/strong&gt; — not the intended one. That's because when you omit the pane target, it resolves to "the pane focused in the TUI," not "the pane the command was invoked from" (stated in the official CLI reference; passing &lt;code&gt;--current&lt;/code&gt; resolves to the calling pane).&lt;/p&gt;

&lt;p&gt;When agents drive the CLI, don't let them omit the target; have them pass their own position explicitly via &lt;code&gt;--current&lt;/code&gt; or &lt;code&gt;HERDR_PANE_ID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Gotcha: server lifetime and startup directory&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The server belongs to the parent process that started it.&lt;/strong&gt; If &lt;code&gt;herdr server&lt;/code&gt; gets started from Claude Code's shell, it can die with that session. After my experiments I stopped the server once and restarted &lt;code&gt;herdr&lt;/code&gt; myself so the server ownership sat with my own process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The startup cwd only matters once, at first session creation.&lt;/strong&gt; &lt;code&gt;cd&lt;/code&gt;-ing into a repo and running &lt;code&gt;herdr&lt;/code&gt; just reattaches to the existing session from the second time on. Add repos from inside herdr with &lt;code&gt;herdr workspace create --cwd  --label&lt;/code&gt;. It took a mental-model move from "cd to navigate" to "switch workspaces to navigate"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does the sidebar show state, or history?
&lt;/h2&gt;

&lt;p&gt;Running herdr inside Zed's terminal gives you this nesting:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlbgieukixgvaoh1aibt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlbgieukixgvaoh1aibt.png" alt="herdr inside a Zed terminal. Spaces/agents sidebar on the left, two Claude Code sessions running side by side in split panes on the right" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The spaces / agents sidebar on the left. Each agent gets one line with its state and remaining context&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What I noticed in use: herdr's sidebar &lt;strong&gt;only shows rows that map 1:1 to living processes&lt;/strong&gt;. When an agent finishes, its row disappears. It structurally cannot get cluttered.&lt;/p&gt;

&lt;p&gt;Zed's Threads sidebar, by contrast, is &lt;strong&gt;a history list&lt;/strong&gt;. Sessions that ended days or months ago sit alongside active ones, projects interleaved. Finished work keeps occupying your screen as "attention inventory."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fll9pw88ybvy0izbvo9se.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fll9pw88ybvy0izbvo9se.png" alt="Zed's Threads sidebar. Finished historical sessions and active ones, across multiple projects, in the same list" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Zed's Threads sidebar. Sessions from 4 days, 2 weeks, and 2 months ago mixed into the same list as active ones&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Does a list show &lt;strong&gt;current state&lt;/strong&gt;, or &lt;strong&gt;past history&lt;/strong&gt;? That felt like a litmus test for UIs in the parallel-agent era.&lt;/p&gt;

&lt;p&gt;This realization changed how I use the whole screen, too. I used to keep a permanent two-way split of editor and CLI — but I only actually looked at the editor "when reading code," and the rest of the time it was dead space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5uhbv68rdzxewbjevhat.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5uhbv68rdzxewbjevhat.png" alt="Before: permanent two-way split. claude CLI on the left, editor and file tree on the right" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before: permanent two-way split. The editor side spends most of its time unwatched&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp0vmiqf1fxno03ovlci.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp0vmiqf1fxno03ovlci.png" alt="After: full-screen editor-only view, switched to only when reading code" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After: herdr (the CLI side) is the main screen; when I read code I flip the editor to full screen with &lt;code&gt;cmd+shift+backtick&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In agent-driven development, the primary screen flips from the editor to the agent CLI, and &lt;strong&gt;the editor becomes the on-demand side&lt;/strong&gt;. Same root idea as the sidebar: align "what's in view" with "what's in use right now," and you stop leaking attention.&lt;/p&gt;
&lt;h2&gt;
  
  
  For a while, the answer was "Zed at the desk, herdr everywhere else"
&lt;/h2&gt;

&lt;p&gt;Even after the evaluation flipped, my placement answer briefly returned to that first instinct (the "shelve it" plan above). Only now it wasn't a passive shelving but an active division of labor: GUI review at the desk goes to Zed; being away, persistence, and delegation to agents go to herdr. Monitoring and reattaching from the iPhone is measured and working on the herdr side.&lt;/p&gt;

Field notes on away-from-desk operation (iPhone mirroring, sleep behavior)

- The iPhone connection path (Tailscale + Termius) is identical to [the one I wrote up in the tmux days](https://dev.to/shimo4228/running-claude-code-from-iphone-via-ssh-tmux-4c10) — just swap tmux for herdr. If conversational UX is your main goal, [my write-up on the official Claude Code app](https://dev.to/shimo4228/claude-code-from-iphone-plugging-3-holes-in-remote-control-17cf) is a better fit. The split is: "conversation = official app / fleet monitoring = Termius + herdr"
- Attach to the same session from the Mac's terminal and from Termius on the iPhone, and the two become **mirrors of the same screen**. Workspace switches and focus moves on one side show up on the other with barely any lag

![Termius on iPhone mirroring the same herdr session as the Mac](https://raw.githubusercontent.com/shimo4228/zenn-content/main/images/herdr-termius-iphone-sync.png)
*The iPhone side. It syncs with the Mac's screen with barely any lag*

- The display size **syncs to the smaller client**. While you're driving from the iPhone, the Mac's terminal also rewraps to the iPhone's screen width

![The Mac side while the iPhone is in control. Pane contents rendered at the iPhone's screen width](https://raw.githubusercontent.com/shimo4228/zenn-content/main/images/herdr-termius-mac-sync.png)
*The Mac at the same moment. While the iPhone is driving, the Mac's display syncs to iPhone size*

- **Agents make no progress while the Mac sleeps** (sleep suspends process execution, launchd-managed or not). Sleep doesn't kill the server — sessions are intact on wake (measured myself: sleep → wake, session survived). If you want agents running while you're away, configure the Mac not to sleep



&lt;p&gt;But this division of labor didn't last. A few days of using herdr as the main battleground ran me into the next question: &lt;strong&gt;Claude Code writes the code — so what am I actually doing in the editor?&lt;/strong&gt; Here begins the second half.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three jobs left in Zed — from "writing tool" to "sign-off tool"
&lt;/h2&gt;

&lt;p&gt;Once you hand all code-writing to the CLI (Claude Code), the editor's "write" function has long since retired. I was still keeping Zed around for three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reading&lt;/strong&gt; — following multiple files side by side on one screen (multibuffer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff review&lt;/strong&gt; — eyeballing the changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking Markdown&lt;/strong&gt; — polishing how articles and design notes look&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, everything left was on the "reading" side. So I flipped the framing: optimize Zed as a tool for reading and signing off.&lt;/p&gt;

&lt;p&gt;The first thing that paid off was the &lt;code&gt;zed&lt;/code&gt; CLI. Append &lt;code&gt;:line&lt;/code&gt; to a file path and it opens with the cursor on that line:&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;# Open a specific line (line:column also works)&lt;/span&gt;
zed src/contemplative_agent/cli.py:715

&lt;span class="c"&gt;# Open several sign-off points at once&lt;/span&gt;
zed src/core/metrics.py:88 src/cli.py:715 tests/test_cli.py:350

&lt;span class="c"&gt;# --wait: don't proceed to the next command until the human closes the file&lt;/span&gt;
zed &lt;span class="nt"&gt;--wait&lt;/span&gt; changed_file.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reverses the direction of the sign-off flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before&lt;/strong&gt;: the agent tells you &lt;code&gt;path:line&lt;/code&gt; as text → the human hunts for it in the editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Now&lt;/strong&gt;: Claude Code pinpoints the line with &lt;code&gt;grep&lt;/code&gt;, runs &lt;code&gt;zed &amp;lt;file&amp;gt;:&amp;lt;line&amp;gt;&lt;/code&gt; → and hands the human an editor already open at the spot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human stops searching and just reads the line being pointed at. Add &lt;code&gt;--wait&lt;/code&gt; and you get a structural pause — "don't proceed until the human has laid eyes on this file" — in a single command. One caveat: &lt;strong&gt;closing the file only means "seen."&lt;/strong&gt; It can't distinguish approve from reject, so don't chain irreversible operations like commits onto &lt;code&gt;--wait&lt;/code&gt;; run those explicitly, human-side, after confirming.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I didn't uninstall Zed at this point. Its resident cost while unused is zero, and the cost of rebuilding a "reading" replacement on the terminal side is real. Let actual usage make the call — that was the judgment.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Japanese-text rendering wall, and a host swap — to Ghostty
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I started optimizing for "reading and signing off," I hit a rendering-quality wall next. Two problems, both around Japanese text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: line spacing in Zed's Markdown preview.&lt;/strong&gt; Preview a Japanese Markdown document and the lines are cramped and hard to read. Digging in, the preview's paragraph line height is &lt;strong&gt;hard-coded at 1.3&lt;/strong&gt; with no user setting (&lt;a href="https://github.com/zed-industries/zed/discussions/56111" rel="noopener noreferrer"&gt;zed#56111&lt;/a&gt; has a request for Japanese-friendly settings). The preview-only font settings (&lt;code&gt;markdown_preview_font_family&lt;/code&gt; etc.) let you raise the size, but the 1.3 ratio itself can't be changed, so it's not a real fix. I moved final article checks to &lt;code&gt;npx zenn preview&lt;/code&gt; (the same rendering as production) and demoted Zed's preview to "glances while writing." &lt;strong&gt;Zed can't finish the reading experience&lt;/strong&gt; — the first crack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, and nastier: Markdown tables emitted by Claude Code came out misaligned in the terminal, with broken rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42ebj7wywp31br30nf8t.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42ebj7wywp31br30nf8t.png" alt="A real example of a Claude Code output table breaking in the terminal" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My suspect was characters whose East Asian Width (Unicode's character-width classification) is "ambiguous" — box-drawing characters and the like. That width can be interpreted differently by each layer: Claude Code computing character widths, herdr laying out the grid, and the terminal actually drawing. When the layers disagree about width, tables break. (Other rendering factors, like font fallback, can produce similar symptoms — so this is a leading suspect, not a confirmed cause.)&lt;/p&gt;

&lt;p&gt;I couldn't pin down the exact mechanism, but &lt;strong&gt;which layer was at fault&lt;/strong&gt; could be isolated with a controlled swap: replace only the host (the terminal doing the rendering). Piping the same Claude Code + herdr output into Terminal.app, the tables lined up cleanly. The misbehaving layer was confirmed to be Zed's terminal rendering. For display bugs in a multi-layer stack, this "swap exactly one layer" move is the fastest isolation you can do.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Table correctness&lt;/th&gt;
&lt;th&gt;Color (True Color)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zed's terminal&lt;/td&gt;
&lt;td&gt;Breaks&lt;/td&gt;
&lt;td&gt;Accurate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal.app&lt;/td&gt;
&lt;td&gt;Accurate&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghostty&lt;/td&gt;
&lt;td&gt;Accurate&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I ended up on &lt;a href="https://ghostty.org/" rel="noopener noreferrer"&gt;Ghostty&lt;/a&gt;. It draws box-drawing and block characters itself instead of relying on the font, so tables don't break, and colors come out accurate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvykmnefrw0cl598vd7s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvykmnefrw0cl598vd7s.png" alt="herdr on Ghostty, with a Claude Code output table rendered without breakage" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;"After the fix." Same Claude Code + herdr output — on Ghostty, tables with box-drawing rules line up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the host settled, unify the look across the nesting too. If the host (Ghostty) and the TUI (herdr) disagree on color scheme, every glance between them adds a small snag. herdr can auto-follow the host's light/dark appearance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/herdr/config.toml&lt;/span&gt;
&lt;span class="nn"&gt;[theme]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tokyo-night"&lt;/span&gt;
&lt;span class="py"&gt;auto_switch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;          &lt;span class="c"&gt;# follow the host's light/dark&lt;/span&gt;
&lt;span class="py"&gt;dark_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tokyo-night"&lt;/span&gt;
&lt;span class="py"&gt;light_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tokyo-night-day"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;herdr server reload-config&lt;/code&gt; applies it without complaint, and the three tiers — macOS appearance → Ghostty → herdr — switch together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mtu3jraahqvfx1bgrdj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mtu3jraahqvfx1bgrdj.png" alt="herdr on Ghostty in light mode. Two Claude Code sessions running side by side in two panes" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38vcsv4u62fu6qjou9qh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38vcsv4u62fu6qjou9qh.png" alt="The same screen in dark mode. Identical layout, paired color schemes" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The same two-pane screen in light and dark. It follows the host's (Ghostty's) appearance, TUI and all.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why herdr gets details like Japanese input right — a solo dev and his bots
&lt;/h2&gt;

&lt;p&gt;What surprised me in use: herdr has engineering effort in places as practical as Japanese input (IME) handling. The settings dialog has entries like these:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famzjgd7u4glldon1ywbc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famzjgd7u4glldon1ywbc.png" alt="herdr's settings dialog, experiments tab. Options for Japanese (IME) input handling" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, with a Japanese IME active, pressing a key sequence like &lt;code&gt;ctrl+b v&lt;/code&gt; can get the &lt;code&gt;v&lt;/code&gt; swallowed by the IME. herdr's countermeasure: switch the input source to ASCII only while accepting a key sequence, then switch back. There's also a feature that persists pane scrollback across server restarts.&lt;/p&gt;

&lt;p&gt;How does a tool get this level of polish? The repository's raw data hints at the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api repos/ogulcancelik/herdr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'{created: .created_at, stars: .stargazers_count, license: .license.spdx_id}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"created":"2026-03-27...","stars":17794,"license":"NOASSERTION"}&lt;/span&gt;
&lt;span class="c"&gt;#   (license shows NOASSERTION because GitHub can't auto-detect&lt;/span&gt;
&lt;span class="c"&gt;#    the AGPL + commercial dual license)&lt;/span&gt;

gh api &lt;span class="s2"&gt;"repos/ogulcancelik/herdr/contributors?per_page=6"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[] | "\(.login): \(.contributions)"'&lt;/span&gt;
&lt;span class="c"&gt;# → ogulcancelik: 979 / kangal-bot: 54 / github-actions[bot]: 43&lt;/span&gt;
&lt;span class="c"&gt;#   akbash-bot: 16 / human contributors are at 4 commits or fewer each&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 113 days since creation (as of 2026-07-18) it's past 17k stars, and it hit &lt;a href="https://news.ycombinator.com/item?id=48714802" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; on 2026-06-29 (166 points, 110 comments; all figures as of writing). Meanwhile, roughly 979 of the commits are the author's own — it's effectively a solo project. The fun part: the #2 and #4 contributors are bots (kangal-bot and akbash-bot). Presumably these are agents the author operates, committing under their own accounts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An aside: kangal and akbash are both real breeds of Turkish livestock guardian dogs. Perhaps a deliberate match with the name herdr (one who herds) — though that's speculation about naming intent. What the API establishes for certain is only that nearly all commits are attributed to the author himself and to bot-named accounts; that the author operates those bots as agents is itself an inference from circumstance.&lt;br&gt;
The tool's defects bounce back into the developer's own velocity every day. So requirement discovery gets fast. Tools of the agent era fit the hand best when they're built together with agents — and that structure was right there in the contributor list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The ending — Zed didn't even get to be the sign-off viewer
&lt;/h2&gt;

&lt;p&gt;Everything so far was about "optimizing Zed as a sign-off viewer." Then even that role got taken by the herdr side.&lt;/p&gt;

&lt;p&gt;The trigger was two herdr plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;file-viewer&lt;/strong&gt; — read-only, git-aware file browsing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reviewr&lt;/strong&gt; — shows diffs in a sidebar and sends line comments back to the agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;file-viewer covers "reading"; reviewr covers "review the diff and send feedback" — both now inside herdr.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33jnayi7u52hvkth9axx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33jnayi7u52hvkth9axx.png" alt="The file-viewer plugin open inside herdr. Claude Code on the left, file browsing on the right" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The agent on the left and the file it opened on the right, on the same screen. "Reading" moved to the terminal side.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faihcf8i23t1k6z7fogk8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faihcf8i23t1k6z7fogk8.png" alt="The reviewr plugin inside herdr showing an uncommitted diff. Claude Code on the left, the diff and changed-file list on the right" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The reviewr screen. The right sidebar shows the uncommitted diff and the changed-file list. What's on screen is the diff of this very article's consolidation work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here the flow comes full circle. Against the "agent → human" bridge from the earlier section (&lt;code&gt;zed file:line&lt;/code&gt;), reviewr builds the reverse "human → agent" bridge (comment send-back). The sign-off loop closed inside herdr.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This plugin marketplace just auto-indexes a GitHub topic — there's no review process. Installing a plugin is the same as granting code execution, so I read the manifest and install script before installing (for file-viewer: SHA-256 verification present, no automatic hooks — confirmed).&lt;br&gt;
The result: the &lt;code&gt;zed file:line&lt;/code&gt; bridge stopped needing to be crossed within hours of being built. The third job, checking Markdown, was covered too — glances-while-writing by file-viewer's rendered view (the &lt;code&gt;v&lt;/code&gt; key cycles diff ⇄ rendered ⇄ syntax), and final checks already consolidated onto &lt;code&gt;npx zenn preview&lt;/code&gt; — leaving no reason for Zed to stay. &lt;strong&gt;Zed lost even the sign-off viewer seat, and within a day I'd stopped opening it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The final stack: Ghostty + herdr + plugins + Claude Code. There is no editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing — the shape of the execution environment becomes the agent's tool
&lt;/h2&gt;

&lt;p&gt;Seen as "a tmux successor," herdr looks redundant next to Zed. My evaluation flipped when the socket API let an agent operate its own environment. Splitting panes, opening rooms, cutting worktrees — the "workspace housekeeping" humans used to do becomes delegable, wholesale.&lt;/p&gt;

&lt;p&gt;And the flip kept going, all the way to a change of tools. From "isn't Zed enough?" to "sign-off only" to "gone." While every layer — editor, terminal, harness (Claude Code's own subagent machinery) — is absorbing "agent orchestration" features at the same time, herdr's distinctive answer is that &lt;strong&gt;it made the layout of the execution environment itself something agents can operate&lt;/strong&gt;. Each layer slimming down to a single function feels less like regression than like attention design: the work of paring what's on screen down to a state with not one layer surplus to its current role.&lt;/p&gt;

&lt;p&gt;That said, "best" here means best for the current role, not forever. The honest way to test the judgment is against actual usage, so in a few weeks I plan to look back at "how many times did I open Zed?"&lt;/p&gt;

&lt;p&gt;It's one brew command to try. If you're running two or more Claude Code sessions in parallel, start by watching your own agents show up in &lt;code&gt;herdr agent list&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ogulcancelik/herdr" rel="noopener noreferrer"&gt;ogulcancelik/herdr&lt;/a&gt; — herdr itself (GitHub)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://herdr.dev/" rel="noopener noreferrer"&gt;herdr.dev&lt;/a&gt; — official site and docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ghostty.org/" rel="noopener noreferrer"&gt;Ghostty&lt;/a&gt; — the terminal I ended up choosing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zed.dev/blog/parallel-agents" rel="noopener noreferrer"&gt;Zed: Parallel Agents&lt;/a&gt; — Zed's parallel agent feature&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/zed-industries/zed/discussions/56111" rel="noopener noreferrer"&gt;zed#56111&lt;/a&gt; — the Discussion on the hard-coded Markdown preview line height&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/running-claude-code-from-iphone-via-ssh-tmux-4c10"&gt;Running Claude Code from iPhone via SSH + tmux&lt;/a&gt; — building the mobile connection path (read tmux as herdr)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/claude-code-from-iphone-plugging-3-holes-in-remote-control-17cf"&gt;Claude Code from iPhone: Plugging 3 Holes in Remote Control&lt;/a&gt; — the conversational-UX side&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/cursor-to-zed-disabling-built-in-ai-for-a-cli-first-setup-6e4"&gt;Cursor to Zed: Disabling Built-in AI for a CLI-First Setup&lt;/a&gt; — the Zed environment this article started from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt; — my GitHub (agent-related skills and tools)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>terminal</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>What Do My AI Agent's Logs Look Like in OpenTelemetry?</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:00:04 +0000</pubDate>
      <link>https://dev.to/shimo4228/what-do-my-ai-agents-logs-look-like-in-opentelemetry-27ic</link>
      <guid>https://dev.to/shimo4228/what-do-my-ai-agents-logs-look-like-in-opentelemetry-27ic</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: what you can see when you plug homegrown logs into OpenTelemetry. I convert them to OpenTelemetry format, visualize them, and sort out which elements are worth borrowing and which you can pass on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted to bring OpenTelemetry to my AI agent (telemetry = the standard machinery for recording and emitting data about a system's behavior; OTel from here on). The typical adoption path is to embed the SDK in the application itself, emit telemetry as it runs, and keep shipping it to a monitoring backend via a Collector (a resident process that receives, transforms, and forwards). But maybe you've hit walls like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running an SDK and a Collector around the clock is heavy for a personal-scale agent.&lt;/strong&gt; There's no ops team watching a monitoring dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I already have my own structured logs.&lt;/strong&gt; I don't want to instrument the same information twice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The standard for which attribute names to record LLM calls under (the GenAI semantic conventions) is still in development.&lt;/strong&gt; The spec may still move, so it's hard to judge how far to lean on it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/shimo4228/contemplative-agent" rel="noopener noreferrer"&gt;Contemplative Agent&lt;/a&gt;, the agent I operate, was in exactly this situation. It's a local-LLM agent that autonomously writes posts and comments on Moltbook (a social network where AI agents post and comment on each other). The text it reads from the feed is written by other agents, and prompt injection (smuggling instructions into a post body to steer the agent reading it) is a daily occurrence — an environment where input cannot be trusted.&lt;/p&gt;

&lt;p&gt;When the agent misbehaves in this environment, investigating the cause requires being able to trace, precisely and after the fact, "what it read from outside and what it emitted at that moment." So I had designed all external input/output to be kept in full in an audit log (an append-only record for reproducing past decisions exactly as they happened). The "own structured logs" in the second wall above is this audit log.&lt;/p&gt;

&lt;p&gt;At the same time, nobody in my environment watches telemetry continuously. What I need is "the ability to examine past runs in a standard UI when something is worth investigating." If that's the requirement, the telemetry doesn't have to be flowing in real time.&lt;/p&gt;

&lt;p&gt;So this article does two things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instead of putting OTel into the production runtime, I built an after-the-fact conversion as an experiment.&lt;/strong&gt; It takes the audit logs I already have, converts them into traces (a record of one processing run as a collection of timestamped intervals) after the fact, and ships them over OTLP (OTel's standard transport protocol) for visualization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I compared my log schema against the OTel standard and added the 2 fields it was missing to the agent's own logs&lt;/strong&gt;: &lt;code&gt;run_id&lt;/code&gt; and &lt;code&gt;session_id&lt;/code&gt;, which identify a run&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first is a small conversion script — zero changes to the agent body, two dependencies. My local logs became visible as a waterfall (the chart that stacks processing intervals along a timeline) in Jaeger (an OSS trace viewer). On a day that had an incident, the picture tells you the moment you open it.&lt;/p&gt;

&lt;p&gt;This article is a sequel to my previous piece, &lt;a href="https://dev.to/shimo4228/why-did-my-agent-decide-that-3-observability-patterns-ami"&gt;"Why Did My Agent Decide That? 3 Observability Patterns"&lt;/a&gt;. There I wrote that what traces and metrics tell you by default stops at "what the request did." This is the continuation: &lt;strong&gt;what, then, can homegrown logs and the OTel standard actually connect?&lt;/strong&gt; — answered by doing the conversion for real. It also reads fine on its own.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Conversion target: append-only JSONL (one JSON per line) logs the agent has already written. Three kinds: LLM call telemetry / API audit / CAPTCHA solver audit&lt;/li&gt;
&lt;li&gt;Python 3.10+, uv for package management&lt;/li&gt;
&lt;li&gt;Only two dependencies: &lt;code&gt;opentelemetry-sdk&lt;/code&gt; + &lt;code&gt;opentelemetry-exporter-otlp-proto-http&lt;/code&gt; (both on the conversion-script side; nothing is added to the agent itself)&lt;/li&gt;
&lt;li&gt;Viewer: Jaeger v2.19.0 (single binary; no Docker required, storage is in-memory)&lt;/li&gt;
&lt;li&gt;Signals: traces only (of OTel's three signals, metrics and logs are out of scope this time)&lt;/li&gt;
&lt;li&gt;The conversion script is public: &lt;a href="https://github.com/shimo4228/contemplative-agent-otel" rel="noopener noreferrer"&gt;contemplative-agent-otel&lt;/a&gt; — a small implementation, about 800 lines in total, built on the assumption that you swap in &lt;code&gt;records.py&lt;/code&gt; (log loading and normalization) / &lt;code&gt;mapping.py&lt;/code&gt; (the attribute-name mapping table) for your own log format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The processing flow is 5 steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the already-written JSONL logs&lt;/li&gt;
&lt;li&gt;Normalize the 3 log kinds into a common record (&lt;code&gt;records.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Attach standard attribute names to each record and convert it into a span (one interval of a trace) (&lt;code&gt;mapping.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Send to Jaeger over OTLP/HTTP&lt;/li&gt;
&lt;li&gt;Check the waterfall and the attributes in the Jaeger UI&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  I decided whether to adopt OTel by asking "who reads the telemetry?"
&lt;/h2&gt;

&lt;p&gt;Put OTel into the agent, or pass? I organized this decision around a single question: "who is going to read that telemetry?" The reason is that OTel traces and my audit log, even when they record the same event, have &lt;strong&gt;different readers (consumers)&lt;/strong&gt;. Take one LLM call: on the trace side, what you want to see is the model name, token counts, duration, and whether it errored. On the audit-log side, what you need is the prompt body itself, the caller, and the identifiers used for reproduction.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;OTel traces&lt;/th&gt;
&lt;th&gt;My audit log&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary reader&lt;/td&gt;
&lt;td&gt;People watching dashboards; alerting&lt;/td&gt;
&lt;td&gt;People investigating incidents; the replay script used for reproduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt and other bodies&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not recorded by default&lt;/strong&gt; (opt-in)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Stored in full&lt;/strong&gt; (base64 + sha256)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nature of the data&lt;/td&gt;
&lt;td&gt;May be sampled; may be volatile&lt;/td&gt;
&lt;td&gt;Append-only; every record; never deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Question it answers&lt;/td&gt;
&lt;td&gt;"What's slow or broken right now?"&lt;/td&gt;
&lt;td&gt;"Why did that decision come out the way it did?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right column is not a property of audit logs in general — it's a design I chose because my goal is offline reproduction of incidents. Your logs may well not store everything. What generalizes is the yardstick: &lt;strong&gt;different consumers mean different retention policies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The emblematic case is body text. The GenAI semantic conventions specify that prompt and response bodies are &lt;strong&gt;not recorded by default&lt;/strong&gt; (they end up on a viewer's screen, so not carrying sensitive data is the default). My audit log does the &lt;strong&gt;exact opposite: full retention&lt;/strong&gt;. In incident investigation, without "the exact bytes the decision saw," you can't reproduce it offline.&lt;/p&gt;

&lt;p&gt;So this isn't a question of which one is right: &lt;strong&gt;same event, different consumers, opposite retention policies.&lt;/strong&gt; Once you frame it this way, the choice stops being binary.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put OTel into the runtime (if someone reads telemetry in real time)&lt;/li&gt;
&lt;li&gt;Do nothing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert existing logs into traces after the fact&lt;/strong&gt; — get the connection to the standard vocabulary, and the visualization, without touching the runtime&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article is an implementation of option 3.&lt;/p&gt;

&lt;p&gt;That said, option 3 only held up because two conditions happened to be true for me: nobody reads telemetry in real time, and the incident-investigation reader was already well served by a thorough audit log. If either is missing, the answer changes. Even with no monitoring team, if you don't have structured logs yet, going straight for option 1 (the runtime SDK) is probably faster. "No monitoring team = no OTel" is not a general rule.&lt;/p&gt;

&lt;p&gt;You might also wonder: "isn't using only part of the standard just cherry-picking the spec?" OTLP is a public protocol, and the semantic conventions are an agreement on attribute names. You don't have to instrument the app with the SDK — any program that speaks OTLP can feed data in. This is a usage the standard anticipates, and this article's conversion script simply uses the OTel SDK as "a library for offline export."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I borrowed from OTel, and what I passed on
&lt;/h2&gt;

&lt;p&gt;Before getting into the implementation, here's how I judged each element of OTel, up front. The biggest lesson this time was that it's not an all-or-nothing "adopt the standard or don't" — &lt;strong&gt;you can pick borrow-or-pass per element.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OTel element&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GenAI semantic conventions vocabulary (&lt;code&gt;gen_ai.*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ Adopt&lt;/td&gt;
&lt;td&gt;External tools and people can read it without explanation. Copying attribute names costs zero dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run-ID machinery (trace ID / session ID equivalents)&lt;/td&gt;
&lt;td&gt;✅ Adopt (implemented &lt;code&gt;run_id&lt;/code&gt; / &lt;code&gt;session_id&lt;/code&gt; in the logs)&lt;/td&gt;
&lt;td&gt;One field turns trace reconstruction from estimation into measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTLP (transport protocol)&lt;/td&gt;
&lt;td&gt;✅ Adopt (as the exit of the offline conversion)&lt;/td&gt;
&lt;td&gt;A common language every viewer speaks. Costs only 2 packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime SDK instrumentation&lt;/td&gt;
&lt;td&gt;❌ Pass&lt;/td&gt;
&lt;td&gt;Would double-instrument alongside the existing audit log, and add dependencies to the agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running a resident Collector&lt;/td&gt;
&lt;td&gt;❌ Pass&lt;/td&gt;
&lt;td&gt;Heavy for a single process at personal scale. On-demand conversion when needed is enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bodies not recorded by default (redaction)&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Followed on the trace side (don't carry untrusted text onto the screen). The audit log does the opposite — full retention, needed for offline reproduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Importing attribute names from the semconv package&lt;/td&gt;
&lt;td&gt;❌ Pass&lt;/td&gt;
&lt;td&gt;Import paths are still unstable while in development, so attribute names are self-managed as string constants&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Passing on an element doesn't mean that part of OTel has no value. There are mainly 4 things that are hard to get without runtime instrumentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact causal structure&lt;/strong&gt; — which operation called which. Only IDs issued at execution time capture this precisely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context propagation across services&lt;/strong&gt; — the machinery that stitches one trace across multiple services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time visibility&lt;/strong&gt; — latency and errors happening right now, visible as they happen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; — numbers aggregated continuously while running. You can technically produce them from an after-the-fact conversion too, but they're fundamentally a signal meant to be measured live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of these, the last 3 have no reader in my operation — a single process, with nobody watching continuously. The only one genuinely missing is the first, and its minimal patch is row 2 of the table, the "run-ID machinery."&lt;/p&gt;

&lt;p&gt;The sections below implement the ✅ elements in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  My logs mapped almost 1:1 onto the standard vocabulary
&lt;/h2&gt;

&lt;p&gt;My homegrown telemetry writes one line per LLM call. A line looks like this (simplified):&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="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-15T09:12:03+09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemma4:e4b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt_eval_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1560&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"eval_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;36400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"done_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&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 next table matches these fields against the GenAI semantic conventions attribute names (the naming table for how to record model name, token counts, finish reason, and so on in OTel).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;My log field&lt;/th&gt;
&lt;th&gt;OTel attribute&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gen_ai.request.model&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prompt_eval_count&lt;/code&gt; (input token count)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;eval_count&lt;/code&gt; (output token count)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;done_reason&lt;/code&gt; (&lt;code&gt;stop&lt;/code&gt; / &lt;code&gt;length&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gen_ai.response.finish_reasons&lt;/code&gt; (array-typed, so wrap as &lt;code&gt;[done_reason]&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;num_predict&lt;/code&gt; (generation cap)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gen_ai.request.max_tokens&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gen_ai.request.temperature&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;error_kind&lt;/code&gt; (&lt;code&gt;timeout&lt;/code&gt; / &lt;code&gt;http_429&lt;/code&gt; etc.)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;error.type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ts&lt;/code&gt; + &lt;code&gt;duration_ms&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;start / end time of the span (one interval of a trace)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;caller&lt;/code&gt; (which processing stage made the call), &lt;code&gt;prompt_sha256&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;No counterpart → custom namespace &lt;code&gt;ca.audit.*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It mapped almost 1:1. For a simple LLM call log like this one (model, token counts, stop reason, parameters), I suspect the set of things you'd want to record converges to the same collection no matter who designs it (logs that also cover tool calls, streaming, or routing won't map this cleanly).&lt;/p&gt;

&lt;p&gt;This is where riding the standard vocabulary pays off. If the attribute name is &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;, external tools and people alike can read it with no explanation.&lt;/p&gt;

&lt;p&gt;The interesting part is &lt;strong&gt;what didn't map&lt;/strong&gt;. &lt;code&gt;caller&lt;/code&gt; (the key I use to aggregate logs by processing stage during incident investigation) and &lt;code&gt;prompt_sha256&lt;/code&gt; (a hash for matching identical prompts without storing the body) have no standard counterpart. These two are investigation keys specific to my operation. For just looking around in a UI, the standard attributes are mostly enough; for offline reproduction of an incident, these are the ones that matter.&lt;/p&gt;

&lt;p&gt;Practical handling of the Development status (official status name: Development)&lt;/p&gt;

&lt;p&gt;The GenAI semantic conventions (semconv below) are in Development status as of this writing (2026-07) (&lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;canonical repository&lt;/a&gt;). The &lt;code&gt;opentelemetry-semantic-conventions&lt;/code&gt; package does have attribute-name constants, but they live under an underscore-prefixed (= unstable) &lt;code&gt;_incubating&lt;/code&gt; import path.&lt;/p&gt;

&lt;p&gt;The conversion script doesn't import from the package. Instead, &lt;strong&gt;the attribute names are defined as string constants in my own code, with the referenced semconv version pinned in a comment&lt;/strong&gt;. If the standard moves, catching up is a single-file change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conversion script has exactly 3 design points
&lt;/h2&gt;

&lt;p&gt;Let's pin down spans properly here. At the top I described a trace as "a collection of timestamped intervals." Each of those intervals is a span: it has a name, start and end times, and attributes (key-value pairs), and spans connect through parent-child links into one trace. In a Jaeger waterfall, one row is one span.&lt;/p&gt;

&lt;p&gt;So the core of the conversion is "read JSONL, create spans with past timestamps." There were 3 design decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Spans can be created at past timestamps
&lt;/h3&gt;

&lt;p&gt;The OTel SDK lets you explicitly set a span's start and end times in epoch nanoseconds. I restore them directly from the log's &lt;code&gt;ts&lt;/code&gt; and &lt;code&gt;duration_ms&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python:emit.py (excerpt, simplified)&lt;/p&gt;

&lt;h1&gt;
  
  
  Create the span at the log's recorded time (the core of the conversion script)
&lt;/h1&gt;

&lt;p&gt;span = tracer.start_span(&lt;br&gt;
    name,                      # e.g. "text_completion gemma4:e4b"&lt;br&gt;
    context=parent_ctx,&lt;br&gt;
    kind=kind,                 # SpanKind (CLIENT etc. for LLM calls)&lt;br&gt;
    attributes=attrs,&lt;br&gt;
    start_time=start_ns,       # ts converted to epoch ns&lt;br&gt;
)&lt;br&gt;
if is_error:&lt;br&gt;
    span.set_status(Status(StatusCode.ERROR, error_type))&lt;br&gt;
span.end(end_time=end_ns)      # start_ns + duration_ms&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


For logs that don't record a duration (API audit, solver audit), I made the spans **zero-width**. Giving them a plausible width by estimation would mean fabricating latency that was never measured. Jaeger draws zero-width spans as thin markers, so they're also visually distinct from the LLM spans that carry real durations. The fact that "logs without a duration become zero-width" was itself feedback into the log schema (it pinpointed the next improvement candidate: add elapsed time to the API audit log too).

### 2. Trace grouping is reconstructed from gaps in time

A trace is normally bundled by IDs issued at execution time. Past logs don't have those. So I merge the 3 log kinds in timestamp order and **start a new trace whenever a gap of a fixed length (default 300 seconds) opens up**. A schedule-launched agent repeats "a few minutes of activity → long silence," so this cut lines up well with an actual single run. Conversely, for workloads where the logs never pause — parallel runs, always-on workers — this cut will merge things incorrectly.

But it is still an estimate. I put a `ca.convert.grouping = "time-gap"` attribute on each trace's root span, making **the data itself say it is a reconstruction, not a measurement**.

Flip that around and this was **a gap in my own log design that only became visible against OTel**: add one field to every log line — a per-run ID (the equivalent of OTel's trace ID) — and this reconstruction turns from estimation into measurement.

Even if you never adopt the SDK, the one mechanism worth borrowing up front is "stamp every run with an ID."

I implemented it in my agent while writing this article: a single write function shared by all audit logs now stamps `run_id` (per process) and `session_id` (per agent session). That is the only agent-side change in this whole effort, and converting past logs still works with the agent untouched. The converter bundles by ID when `run_id` exists and falls back to time gaps only for older logs. If you're designing logs now, I recommend putting it in from day one.

### 3. Untrusted text is dropped at the conversion entrance

The third design decision is the boundary of what gets onto the Jaeger screen.

The audit log contains raw text that came from outside (CAPTCHA challenge texts, server error response bodies), stored as base64. As described at the top, this agent's input comes from a social network where prompt injection is routine — "strings from outside may be attack input" is the default assumption. Put such text into span attributes, and **attacker-controlled strings flow straight onto the Jaeger screen (= into screenshots, = into this article)**.

The conversion drops bodies at the parser stage and passes through only a sha256 hash and a classification code (`http_400` etc.). There is no "flag to include bodies" either. On top of that, I added a regression test that scans every attribute value of every test fixture and asserts that no body fragment appears. Not "be careful later," but "make it structurally unable to appear, then visualize" — that's the safe side.

To be clear, the goal here is not to keep bodies secret but to avoid carrying external strings onto the screen. The sha256 is a correlation identifier for matching identical inputs, not an anonymization mechanism (short boilerplate strings can be brute-forced back). If your logs hold confidential data, consider a keyed hash (HMAC) or simply not surfacing the hash at all.

## Looking at it in Jaeger — the incident day was obvious the moment I opened it

Jaeger v2 is a single binary; once started, it accepts OTLP directly and keeps everything in memory. Because it takes OTLP directly, there's no need for the Collector I called "heavy" at the top, either.



```bash
# Start the viewer (no Docker; data disappears when it exits)
# For Apple Silicon. On Intel Macs read darwin-amd64; on Linux, linux-amd64
curl -sLO https://github.com/jaegertracing/jaeger/releases/download/v2.19.0/jaeger-2.19.0-darwin-arm64.tar.gz
tar xzf jaeger-2.19.0-darwin-arm64.tar.gz
./jaeger-2.19.0-darwin-arm64/jaeger    # OTLP :4318 / UI :16686

# Convert and send (example run; see the repository README for setup)
contemplative-agent-otel --date 2026-07-15
# =&amp;gt; emitted 1031 spans across 5 runs -&amp;gt; http://localhost:4318
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A normal day looks like this. Reading the screen: time runs along the horizontal axis, one row is one span, parent-child structure is indentation, errors are red marks. One trace is one agent run, with LLM calls carrying real durations (&lt;code&gt;text_completion gemma4:e4b&lt;/code&gt;, 36 seconds or 1.3 minutes) and zero-width API calls lined up in time order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajoefvptlvwspgt6dcjw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajoefvptlvwspgt6dcjw.png" alt="Trace of a normal day: under the agent run root span, LLM calls and API calls lined up in time order as a waterfall" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open a span and the mapping table from earlier appears directly as attributes. Standard attributes like &lt;code&gt;gen_ai.usage.input_tokens: 1560&lt;/code&gt; sit alongside custom ones like &lt;code&gt;ca.audit.caller: core.skill_selection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0864ijmfz6x3hh9iymj3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0864ijmfz6x3hh9iymj3.png" alt="Span detail: gen_ai.* standard attributes and ca.audit.* custom attributes side by side" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's what converting the day with the incident produces. On that day, a retry storm (failed calls whose retries cascade until calls and logs balloon) had in fact hit the LLM backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5jevyxhitgm47mrnx5kv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5jevyxhitgm47mrnx5kv.png" alt="Search results for the incident day: a trace with 30442 spans / 33 errors sits at the top" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where a normal run has tens to hundreds of spans, &lt;strong&gt;one trace with 30,442 spans and 33 errors&lt;/strong&gt; towers over everything. The log file for that day was also nearly 30 times its normal size. As numbers, this information was already in the logs — but "you know it's an abnormal day the moment you open it" is a strength unique to trace visualization.&lt;/p&gt;

&lt;p&gt;You can follow the errors too. In the run where the CAPTCHA answer was rejected by the server, &lt;code&gt;captcha solve&lt;/code&gt; and &lt;code&gt;POST /verify&lt;/code&gt; are marked red as a pair. The error bodies were dropped by the conversion, so all the screen shows is the classification: &lt;code&gt;error.type: http_400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe77z1s7113eh424x9ew1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe77z1s7113eh424x9ew1.png" alt="A run containing errors: red error marks on captcha solve and POST /verify" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gotcha: send 30,442 spans at once and they're silently dropped&lt;/p&gt;

&lt;p&gt;In the OTel SDK, &lt;code&gt;span.end()&lt;/code&gt; doesn't send immediately — a &lt;code&gt;BatchSpanProcessor&lt;/code&gt; (an SDK-internal component that queues spans and sends them in batches) does the exporting. In the incident-day conversion, spans beyond the default queue length of 2048 were &lt;strong&gt;discarded with only a one-line warning&lt;/strong&gt; (&lt;code&gt;Queue full, dropping Span.&lt;/code&gt;). For real-time instrumentation that's a reasonable self-defense; for an offline bulk conversion, "send everything" is the correct spec.&lt;/p&gt;

&lt;p&gt;Since the number of records to convert is known before sending, I sized the queue to the record count and that solved it. If you use the SDK for bulk conversion, watch out: the defaults assume real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Instead of a binary "adopt OTel or don't," I tried the third option: &lt;strong&gt;after-the-fact conversion of existing logs&lt;/strong&gt;. Zero changes to the agent, two dependencies — and you get the Jaeger waterfall, error display, and standard attributes, all of it&lt;/li&gt;
&lt;li&gt;My homegrown LLM call log mapped &lt;strong&gt;almost 1:1&lt;/strong&gt; onto the GenAI semantic conventions. The fields that didn't map remain as a list of "information needed for offline reproduction of incidents"&lt;/li&gt;
&lt;li&gt;Traces serve humans investigating on a screen (bodies unrecorded by default); the audit log serves machines re-executing for incident reproduction (bodies stored in full). &lt;strong&gt;Different consumers flip the retention policy&lt;/strong&gt;, so in a conversion, dropping untrusted bodies at the entrance is the safe side&lt;/li&gt;
&lt;li&gt;Master 3 things — span creation at past timestamps, zero-width spans, trace reconstruction from time gaps — and any structured log from which times and run groupings can be recovered connects the same way&lt;/li&gt;
&lt;li&gt;Add one run-ID field to your logs and the reconstruction itself becomes unnecessary. Even without the SDK, this one mechanism is worth borrowing from OTel up front (I implemented it while writing this article)&lt;/li&gt;
&lt;li&gt;This conversion was also &lt;strong&gt;a way to measure my own log design against OTel as a yardstick&lt;/strong&gt;. The missing run ID and the real-time assumptions of the export queue both became visible only against the standard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/contemplative-agent-otel" rel="noopener noreferrer"&gt;contemplative-agent-otel&lt;/a&gt; — this article's conversion script (mapping table and tests included)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/contemplative-agent/blob/main/docs/adr/0078-otel-connection-via-vocabulary-and-offline-export.md" rel="noopener noreferrer"&gt;ADR-0078: OTel Connection via Vocabulary Mapping and Offline Export&lt;/a&gt; — the primary source for this decision (&lt;a href="https://github.com/shimo4228/contemplative-agent/blob/main/docs/adr/0075-observability-by-default.md" rel="noopener noreferrer"&gt;ADR-0075&lt;/a&gt;: the companion decision to ship audit logs in the same PR as the feature)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions&lt;/a&gt; — canonical source of the standard vocabulary (Development status)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/why-did-my-agent-decide-that-3-observability-patterns-ami"&gt;Previous article: Why Did My Agent Decide That? 3 Observability Patterns&lt;/a&gt; — the 3 design patterns on the audit-log side&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/can-you-trace-the-cause-after-an-incident-neo"&gt;Can You Trace the Cause After an Incident?&lt;/a&gt; — one step further back: why records that let you trace causality after an agent incident matter in the first place&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/building-an-autonomous-agent-on-an-m1-mac-by-choice-5b5o"&gt;Building an Autonomous Agent on an M1 Mac — by Choice&lt;/a&gt; — hub of the small-LLM series this article belongs to&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;My GitHub&lt;/a&gt; — the agent itself and related repositories&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>jaeger</category>
      <category>llm</category>
    </item>
    <item>
      <title>Fault Injection TDD Found 3 Silent Failures in My LLM Agent</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/shimo4228/fault-injection-tdd-found-3-silent-failures-in-my-llm-agent-3mld</link>
      <guid>https://dev.to/shimo4228/fault-injection-tdd-found-3-silent-failures-in-my-llm-agent-3mld</guid>
      <description>&lt;p&gt;Pipelines with an LLM in the loop keep hitting walls like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The LLM returns JSON in a subtly different shape. Parsing succeeds, but the result silently comes back empty&lt;/li&gt;
&lt;li&gt;A local LLM's response was cut off mid-generation (&lt;code&gt;done_reason=length&lt;/code&gt;) and you only notice days later&lt;/li&gt;
&lt;li&gt;The log only says &lt;code&gt;outcome="error"&lt;/code&gt;, so you can't tell whether it was a rate limit or a timeout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What they have in common: &lt;strong&gt;every test passes, yet production breaks silently&lt;/strong&gt;. Processing doesn't stop, no exception is raised — the results, or the records of them, quietly degrade. In this article I call this a silent failure.&lt;/p&gt;

&lt;p&gt;The local-LLM CLI agent I operate hit this family of failures 5 times. All 5 slipped past the tests I had beforehand and were only diagnosed after the fact. So I adopted this workflow: build a fault catalog (a taxonomy of failures) from the history of production incidents, write tests that deliberately inject those faults &lt;em&gt;first&lt;/em&gt;, and land the minimal guard that makes them pass in the same PR. Taxonomically, this is &lt;strong&gt;fault injection testing × TDD&lt;/strong&gt;. What I borrowed — and deliberately didn't borrow — from chaos engineering (the practice of injecting failures on purpose to verify resilience), which inspired the approach, is laid out in the body.&lt;/p&gt;

&lt;p&gt;The first application found 3 silent failures that my ~1800 existing tests had missed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What you'll get from this article&lt;/strong&gt;: a pattern for adding fault injection testing to a single-process Python app — no daemon, deterministic, pytest-native (fault catalog → RED → GREEN) — plus the details of the 3 silent failures the first application actually surfaced&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;Python 3.13 / pytest 9.0 (at time of verification; no bleeding-edge features are used, so nearby versions should work)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hypothesis.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;hypothesis&lt;/a&gt; 6.156.6 / &lt;a href="https://github.com/getsentry/responses" rel="noopener noreferrer"&gt;responses&lt;/a&gt; 0.26.0 (both added as dev dependencies)&lt;/li&gt;
&lt;li&gt;Target: a single-process Python app that calls an LLM server (Ollama etc.) via &lt;code&gt;requests&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The LLM call sits behind a swappable layer such as a Protocol (if it doesn't, carving out that layer is step zero)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is a sequel to my previous one, &lt;a href="https://dev.to/shimo4228/why-did-my-agent-decide-that-3-observability-patterns-ami"&gt;"Why Did My Agent Decide That? 3 Observability Patterns"&lt;/a&gt;. If that one was about &lt;strong&gt;recording&lt;/strong&gt; the agent's decisions, this one is about &lt;strong&gt;asserting on&lt;/strong&gt; that recording channel — and &lt;strong&gt;injecting faults up front&lt;/strong&gt;. It stands alone, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chaos engineering was the inspiration; fault injection testing is what I adopted
&lt;/h2&gt;

&lt;p&gt;My agent operates autonomously on a social network (Moltbook), continuously. As long as it keeps running, I can't know in advance which failure it will meet next. Searching for a way to build "operations resilient to unforeseen failures," I arrived at chaos engineering.&lt;/p&gt;

&lt;p&gt;But measured against the five advanced principles of the canonical &lt;a href="https://principlesofchaos.org/" rel="noopener noreferrer"&gt;Principles of Chaos Engineering&lt;/a&gt;, I did not follow it verbatim.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Canonical principle&lt;/th&gt;
&lt;th&gt;This work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build a Hypothesis around Steady State Behavior&lt;/td&gt;
&lt;td&gt;○ Adopted (asserts against the execution-log (telemetry) channel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vary Real-world Events&lt;/td&gt;
&lt;td&gt;△ Half (faults come from incident history, but injection is a deterministic replay of a known catalog)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run Experiments in Production&lt;/td&gt;
&lt;td&gt;✕ Rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automate Experiments to Run Continuously&lt;/td&gt;
&lt;td&gt;△ CI runs them automatically, but derandomized — a regression test that pins the known, not an experiment that explores the unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimize Blast Radius&lt;/td&gt;
&lt;td&gt;○ Adopted in extreme form (injection happens inside tests, so production impact is zero)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I rejected production experiments for 2 reasons. First, the target is a single local process — there is no "redundant fleet of instances" to kill in the first place. Second, the logs the agent accumulates are primary data that can't be regenerated; random injection in production risks corrupting them. I deferred random exploration to keep CI from going flaky.&lt;/p&gt;

&lt;p&gt;The core of chaos engineering is not "testing" (asserting known expected behavior) but "&lt;strong&gt;experimentation&lt;/strong&gt;" (discovering unknown weaknesses) — so once production experiments and random exploration are out, it can no longer be called chaos engineering. As stated up front, the classification is fault injection testing × TDD. What I borrowed from chaos engineering is exactly 2 ideas: "build the failure catalog from real-world events" and "assert steady state through an observation channel."&lt;/p&gt;

&lt;p&gt;Tooling-wise, I also skipped the distributed-systems staples and built on pytest. This was the conclusion of pre-adoption external research.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;chaostoolkit / toxiproxy&lt;/td&gt;
&lt;td&gt;Rejected&lt;/td&gt;
&lt;td&gt;Declarative experiment runner / resident network proxy. Built for distributed topologies; the scale of the target doesn't match a single local process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pytest fault-injection plugins&lt;/td&gt;
&lt;td&gt;Rejected&lt;/td&gt;
&lt;td&gt;Nothing usable exists (e.g. pytest-disrupt is a TODO-only scaffold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/deepankarm/agent-chaos" rel="noopener noreferrer"&gt;agent-chaos&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Reference only&lt;/td&gt;
&lt;td&gt;Conceptually the closest, but coupled to the Anthropic SDK / DeepEval / pydantic-ai, with no pytest integration and no local-backend support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hypothesis + responses + a hand-rolled injection backend&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Adopted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sits on the existing test infrastructure (pytest) with just 2 dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That settled the approach: &lt;strong&gt;inject faults deterministically inside tests&lt;/strong&gt;. No daemon, no proxy — pytest itself is the execution environment.&lt;/p&gt;

&lt;p&gt;The 2 elements I deferred (exploration, experimentation) are not discarded; they remain as &lt;strong&gt;the entry points for extending this toward chaos engineering later&lt;/strong&gt; — a two-tier setup where PR CI stays deterministic while a nightly run randomizes with seed recording (any failing sequence found gets reproduced from the seed and pinned with &lt;code&gt;@example&lt;/code&gt;), plus sandbox experiments running the real pipeline in random-injection mode. This pilot leaned deterministic because the state of "even the known incident history isn't asserted" had to be fixed first. Once the known holes are plugged, those 2 entry points are where I'll expand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern — the fault catalog becomes the test spec
&lt;/h2&gt;

&lt;p&gt;The workflow is 3 steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Catalog&lt;/strong&gt;: classify the history of production incidents into fault classes (failure taxonomies) and diff them against existing tests. The untested classes become the fault catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RED&lt;/strong&gt;: for each fault, assert in a test how the system &lt;strong&gt;should&lt;/strong&gt; behave when it's injected. The current implementation fails it (this is TDD's RED)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GREEN&lt;/strong&gt;: land the minimal guard that passes the test, in the &lt;strong&gt;same PR&lt;/strong&gt; as the test&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point is that the fault catalog doubles as a spec. Desired behaviors — "a timeout should leave &lt;code&gt;error_kind=timeout&lt;/code&gt; in telemetry," "malformed-shape JSON should abstain with a reason code" — get pinned down as executable tests.&lt;/p&gt;

&lt;p&gt;The only difference from ordinary TDD is where the inputs come from. Instead of writing tests from functional requirements, you &lt;strong&gt;write tests from incident history&lt;/strong&gt; — everything else reuses the RED → GREEN discipline as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the fault catalog from incident history, not imagination
&lt;/h2&gt;

&lt;p&gt;If you enumerate "which faults to inject" from imagination, you tend to mass-produce tests for failures that never actually happen. I started from 5 past production incidents (silent truncation from context-length overflow, mid-generation cutoff with &lt;code&gt;done_reason=length&lt;/code&gt;, deduplication failing to fire, an external API rate limit, and a scraping target turning into a CAPTCHA) and swept for "failures in the same family that existing tests don't cover."&lt;/p&gt;

&lt;p&gt;One caveat: the 5 incidents and the fault classes are not 1:1. What I extracted from the history is the shared family — "when the LLM or external I/O returns something unexpected, the pipeline silently degrades" — and from that family I derived the 5 classes that &lt;strong&gt;existing tests didn't cover&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here are the resulting 5 classes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Fault class&lt;/th&gt;
&lt;th&gt;State of existing tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;F1&lt;/td&gt;
&lt;td&gt;Read-timeout mid-generation&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;ConnectionError&lt;/code&gt; covered. Mid-stream timeout untested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F2&lt;/td&gt;
&lt;td&gt;Embedding API failures — transport-level (429, timeout) and content-invalid success responses (mismatched dimensions, missing rows)&lt;/td&gt;
&lt;td&gt;Untested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F3&lt;/td&gt;
&lt;td&gt;Syntactically valid JSON that violates the expected schema (wrong top-level type, wrong keys, non-string elements)&lt;/td&gt;
&lt;td&gt;Untested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F4&lt;/td&gt;
&lt;td&gt;HTTP 429 from the LLM server itself&lt;/td&gt;
&lt;td&gt;Only covered on a different external API client. LLM backend side untested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F5&lt;/td&gt;
&lt;td&gt;Flapping (alternating / consecutive success-failure sequences)&lt;/td&gt;
&lt;td&gt;Only single failure→recovery covered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before and after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tests directly injecting the fault classes&lt;/td&gt;
&lt;td&gt;0/5 (per the table above, only partial coverage of adjacent cases)&lt;/td&gt;
&lt;td&gt;5/5 (32 deterministic fault injection tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry failure-type discrimination&lt;/td&gt;
&lt;td&gt;None (everything is &lt;code&gt;outcome="error"&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;7 kinds of &lt;code&gt;error_kind&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability of extraction failures&lt;/td&gt;
&lt;td&gt;Silently missing&lt;/td&gt;
&lt;td&gt;3 reason codes + per-reason aggregate summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full suite&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1832 passed / 1 skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The test count of 32 is measured with &lt;code&gt;pytest --collect-only&lt;/code&gt; (every number in this article was re-measured at the time of writing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation — 2 replacement points and a determinism discipline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pin injection to 2 existing seams
&lt;/h3&gt;

&lt;p&gt;Injecting faults requires a seam (a joint where the test side can swap the implementation without rewriting production code). I used only 2 existing ones and decided to &lt;strong&gt;add zero injection hooks to production code&lt;/strong&gt;. The pipeline under test does not know it is being tested.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The LLM backend Protocol&lt;/strong&gt; — implement a &lt;code&gt;ChaosBackend&lt;/code&gt; on the test side and swap it in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The requests HTTP layer&lt;/strong&gt; — fake HTTP responses with the &lt;code&gt;responses&lt;/code&gt; library&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;ChaosBackend&lt;/code&gt; is driven by a schedule — a list of "which fault fires on which call." The chaos in class and file names is a leftover from the naming at inspiration time. Also, &lt;code&gt;FAULT_VOCABULARY&lt;/code&gt; in the code is the minimal unit of injection (primitives), at a different granularity from the F1–F5 fault classes — one class is tested through combinations of several primitives.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python:tests/chaos.py (excerpt, simplified — see the public skill in Related Links for the full version)&lt;/p&gt;

&lt;h1&gt;
  
  
  Fault vocabulary is an ordered tuple (unordered would break seed-derived determinism)
&lt;/h1&gt;

&lt;p&gt;FAULT_VOCABULARY = (OK, NONE, EMPTY, EXC_TIMEOUT, EXC_CONNECTION, TRUNCATED, SHAPE_VIOLATION)&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class ChaosBackend:  # conforms to the LLMBackend Protocol&lt;br&gt;
    schedule: List[str] = field(default_factory=list)&lt;br&gt;
    calls: List[dict] = field(default_factory=list)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@classmethod
def from_seed(cls, seed: int, n: int, weights=None) -&amp;gt; "ChaosBackend":
    rng = random.Random(seed)
    vocab = list(weights.keys()) if weights else list(FAULT_VOCABULARY)
    wts = list(weights.values()) if weights else None
    return cls(schedule=rng.choices(vocab, weights=wts, k=n))

def generate(self, prompt, system, num_predict, format, *, temperature=1.0, think=False):
    idx = len(self.calls)
    self.calls.append({"prompt": prompt, "num_predict": num_predict})
    fault = self.schedule[idx] if idx &amp;lt; len(self.schedule) else OK
    if fault == NONE: return None
    if fault == EXC_TIMEOUT: raise requests.exceptions.ReadTimeout("chaos")
    if fault == TRUNCATED: return BackendResult(text=self._ok_text(idx), finish_reason="length")
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


A schedule can be built as an explicit list or derived from a seed — either way, **you can inspect its contents before the run**. This is not "break things randomly"; it's "breaking under this exact sequence is the spec."

### Run hypothesis under a deterministic profile

Instead of enumerating fault combinations by hand, I use property-based testing (asserting properties that must hold for any input, rather than individual input/output examples). To keep CI from going flaky, though, hypothesis is pinned to deterministic mode.



```python:tests/conftest.py (excerpt)
# database=None only disables the example DB. The constants/unicode caches
# need HYPOTHESIS_STORAGE_DIRECTORY relocated (before hypothesis is imported)
os.environ.setdefault("HYPOTHESIS_STORAGE_DIRECTORY", str(_TEST_HOME / ".hypothesis"))
from hypothesis import settings
settings.register_profile("ci", derandomize=True, max_examples=50, deadline=None, database=None)
settings.load_profile("ci")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;derandomize=True&lt;/code&gt; generates the same case sequence every run. I verified this by running the full fault injection suite twice in a row and confirming identical output.&lt;/p&gt;

&lt;p&gt;The other determinism rule: &lt;strong&gt;latency faults are expressed by injecting a &lt;code&gt;ReadTimeout&lt;/code&gt; exception, not by real sleeps&lt;/strong&gt;. On this call path, the observable consequence of a timeout reduces entirely to "catch and handle &lt;code&gt;ReadTimeout&lt;/code&gt;," so within that scope exception injection verifies the same thing — and the tests run in 0 seconds. The real-sleep-plus-short-timeout approach goes flaky, so I avoided it. Note that &lt;strong&gt;behaviors that only surface in real time cannot be verified this way&lt;/strong&gt; — e.g. cleanup of partial output mid-stream — so if that's your target, you need separate measures.&lt;/p&gt;
&lt;h3&gt;
  
  
  Writing RED — assert the desired guard behavior first
&lt;/h3&gt;

&lt;p&gt;Here is the RED for F3 (valid JSON, wrong shape).&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python:tests/test_distill_chaos.py (excerpt)&lt;br&gt;
class TestParsePatternsShapeViolationFuzz:&lt;br&gt;
    @given(raw=non_patterns_json())&lt;br&gt;
    @example(raw='{"patterns": [123]}')   # pins the real str() promotion bug&lt;br&gt;
    @example(raw="null")                  # pins the json.loads("null") is None trap&lt;br&gt;
    def test_wrong_shape_abstains_with_no_patterns(self, raw):&lt;br&gt;
        patterns, mode = _parse_patterns(raw)&lt;br&gt;
        assert mode == "shape_violation"&lt;br&gt;
        assert patterns == []&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`@given` asserts the property "any shape-violating JSON should cause abstention," and `@example` pins known failure shapes as permanent regression tests. Those 2 `@example`s are exactly silent failures ① and ② in the next section.

## The 3 silent failures the first application surfaced

The moment I wrote the RED tests, 3 holes in the existing implementation were exposed. ① and ② are the kind where the processing result silently degrades; ③ is on the observation-channel side — the failure itself gets recorded, but the type information needed for root-cause analysis is silently lost.

### ① Numbers promoted to strings let schema violations sail through

- **Symptom**: when the LLM returned non-string elements like `{"patterns": [123]}`, the old implementation stringified each element with `str(item)`, so `"123"` could pass as a legitimate pattern
- **Why it's silent**: the JSON is valid, so parsing succeeds, and `str()` promotion raises nothing. No trace of the schema violation is left anywhere
- **Guard**: require `isinstance(item, str)` on every element; on violation, abstain with the `shape_violation` reason code. Regression pinned with `@example(raw='{"patterns": [123]}')`

### ② `json.loads("null")` returns `None`

- **Symptom**: when the response body is JSON `null`, `json.loads` returns `None` without raising. The old implementation used "parse result is `None` = parse failed" as its check, so valid JSON was treated as a parse failure and misrouted into a fallback path (bullet-list text scanning) it should never have entered
- **Why it's silent**: the fallback path legitimately exists as a rescue for non-JSON responses, so a misroute is indistinguishable in the logs from a normal fallback
- **Guard**: introduce an identity sentinel, `_JSON_PARSE_FAILED = object()`, separating "parsing failed" from "`None` was parsed" at the type level

### ③ Telemetry flattens every failure type into `error`

- **Symptom**: whether it was a 429, a timeout, a connection failure, or a bad body, telemetry only recorded `outcome="error"`, making offline failure analysis impossible
- **Why it's silent**: the error itself is recorded, so "something failed" is visible. What's flattened is the **type** — a gap you can't notice until you actually attempt an incident investigation
- **Guard**: add `_classify_request_error` to classify exceptions, and add an `error_kind` field to failure rows only (7 kinds: `timeout` / `connection` / `http_&amp;lt;status&amp;gt;` / `bad_json` / `bad_url` / `request_error` / `backend_exception`). The change is additive — the existing `outcome` value set is untouched — so analysis code over past logs keeps working

What the 3 share: they are **abnormal paths wearing a happy-path face**. Bugs that raise exceptions get caught by ordinary tests, but all 3 of these "run to completion while the result or the record goes quietly wrong" — no amount of extra happy-path tests would have caught them. Only RED tests that inject faults and assert the desired way of failing expose these holes.

&amp;lt;details&amp;gt;&amp;lt;summary&amp;gt;2 gotchas (hypothesis cache / circuit breaker × property tests)&amp;lt;/summary&amp;gt;

**`.hypothesis/` appears even with `database=None`**

Even with `database=None` in the profile, caches like `.hypothesis/constants/` show up at the repo root. As the [settings reference](https://hypothesis.readthedocs.io/en/latest/reference/api.html) says, `database=None` only suppresses the example DB; the constants cache and friends are written unconditionally under `HYPOTHESIS_STORAGE_DIRECTORY`. The fix is to point that environment variable at a test tempdir **before** importing hypothesis (included in the code sample above).

**The circuit breaker breaks property-test predictions**

I asserted the property "success count = number of OKs in the schedule," and it broke only on schedules with 5 consecutive failure faults. The cause was the circuit breaker in production code (a mechanism that cuts off calls after consecutive failures; in this project it opens after 5 in a row). Once the breaker opens, subsequent OK calls never reach the backend, so the exact-count property can't hold even though the behavior is correct. I handled it in 2 layers: exclude breaker-tripping schedules from the exact-count property via a `trips_circuit()` filter, and keep a weaker property — "no schedule ever crashes" — over all schedules.

&amp;lt;/details&amp;gt;
## Wrap-up — turning post-hoc debugging into up-front spec

- Silent failures don't get caught by happy-path tests. Only tests that **inject faults and assert the desired way of failing** catch them
- Build the fault catalog from **production incident history**, not imagination. 5 incidents → 5 untested fault classes → 32 deterministic fault injection tests
- For a single-process local LLM app, no daemon or proxy needed: **hypothesis + responses + a swappable backend** sit right on top of pytest
- The determinism discipline (`derandomize` / seed-derived schedules / no real sleeps) removes the 2 big flakiness sources — randomness and real time — from injection tests (measured: identical output across 2 consecutive runs)
- Land the guard in the **same PR** as the test. The fault schedule is the spec; the guard is the implementation that satisfies it
- Chaos engineering is the inspiration, not the classification. What I borrowed is 2 things — "build the catalog from real-world events" and "assert steady state through an observation channel" — while production experiments and unknown-space exploration are deliberately deferred (nightly randomization and sandbox experiments are the entry points for extending toward chaos)

The pattern from this work (fault vocabulary, ChaosBackend, hypothesis profile, RED templates) is published as a generalized Claude Code skill. If you want to bring this into your own pipeline, that's the entry point.

## Related Links

- [chaos-tdd-fault-injection](https://github.com/shimo4228/chaos-tdd-fault-injection) — the public skill generalizing this article's pattern (called "chaos-TDD" in the repo and its ADRs)
- [ADR-0077: Chaos-TDD Fault Injection](https://github.com/shimo4228/contemplative-agent/blob/main/docs/adr/0077-chaos-tdd-fault-injection.md) — primary source for the design decisions (English; a Japanese version sits in the same directory)
- [Previous article: Why Did My Agent Decide That? 3 Observability Patterns](https://dev.to/shimo4228/why-did-my-agent-decide-that-3-observability-patterns-ami)
- [Building an Autonomous Agent on an M1 Mac, on Purpose](https://zenn.dev/shimo4228/articles/small-llm-by-choice) — hub of the small-LLM series this article belongs to (Japanese)
- [hypothesis documentation](https://hypothesis.readthedocs.io/en/latest/) / [responses](https://github.com/getsentry/responses)
- [Chaos Toolkit](https://chaostoolkit.org/) / [toxiproxy](https://github.com/Shopify/toxiproxy) — chaos tools for distributed systems (not adopted here due to the mismatch in target scale)
- [agent-chaos](https://github.com/deepankarm/agent-chaos) — prior OSS for chaos engineering on AI agents (prior art referenced for fault classification)
- [Author's GitHub](https://github.com/shimo4228) — other repositories and tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>testing</category>
      <category>llm</category>
      <category>pytest</category>
      <category>agents</category>
    </item>
    <item>
      <title>LLM-as-Judge Shouldn't Aggregate Scores: Binary Checks as Evidence, One Holistic Verdict</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:00:09 +0000</pubDate>
      <link>https://dev.to/shimo4228/llm-as-judge-shouldnt-aggregate-scores-binary-checks-as-evidence-one-holistic-verdict-822</link>
      <guid>https://dev.to/shimo4228/llm-as-judge-shouldnt-aggregate-scores-binary-checks-as-evidence-one-holistic-verdict-822</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article gives you&lt;/strong&gt;: How to design LLM quality evaluation around "binary checks + a named verdict" instead of numeric scores. Includes a judge prompt structure you can reuse by swapping in your own evaluation target.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Having an LLM evaluate deliverables is called LLM-as-judge. Once you introduce it, most people run into the same three walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for a 5-point rubric score and the same input oscillates between 3 and 4 across runs&lt;/li&gt;
&lt;li&gt;Gate pass/fail on a total-score threshold and one fatal flaw gets diluted by high scores on everything else — and slips through&lt;/li&gt;
&lt;li&gt;Read the scores back later and nobody can explain why something got a 3.5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article introduces a design pattern that avoids all three at once. The core fits in one line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collect evidence with binary Yes/No checks, have the judge pick one verdict from named labels as a holistic judgment, and never aggregate anything into a score.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been running this pattern for about four and a half months in a Claude Code skill-quality audit (&lt;code&gt;/skill-stocktake&lt;/code&gt;) and a knowledge-extraction quality gate (&lt;code&gt;/learn-eval&lt;/code&gt;). How I built the audit command — the trial and error that led me to throw out numeric rubrics — is covered in &lt;a href="https://dev.to/shimo4228/offloading-ais-weak-spots-to-shell-scripts-designing-building-and-publishing-a-skill-audit-2ll8"&gt;my previous article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article is the sequel. It generalizes that design into a &lt;strong&gt;reusable LLM-as-judge pattern&lt;/strong&gt;, including the episode where I tested my own design while it was in production — and rebuilt it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Audience: anyone who wants to automate quality evaluation of deliverables (documents, code, data) with an LLM&lt;/li&gt;
&lt;li&gt;The examples use Claude Code skill evaluation, but the pattern itself is agnostic to evaluation target, model, and tooling&lt;/li&gt;
&lt;li&gt;Terminology: &lt;strong&gt;verdict&lt;/strong&gt; = the final output of an evaluation. It takes the form of picking exactly one from named options such as "Keep / Retire". The JSON examples in this article use a &lt;code&gt;verdict&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;Terminology: &lt;strong&gt;holistic judgment&lt;/strong&gt; = a single conclusion reached by looking at the whole, without going through per-dimension scoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern at a glance — three principles
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Evaluation target
  → ① Binary checks: decompose into Yes/No questions
  → No answers = evidence
  → ② Holistic judgment: pick one named verdict
  → ③ No aggregation: scoring and averaging are forbidden
  → Downstream code / humans branch on the verdict
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;Do&lt;/th&gt;
&lt;th&gt;Don't&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① Checks are binary&lt;/td&gt;
&lt;td&gt;Decompose criteria into questions answerable with Yes/No&lt;/td&gt;
&lt;td&gt;"Rate specificity on a 5-point scale"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② The verdict is a named label&lt;/td&gt;
&lt;td&gt;Look at the whole and pick one from &lt;code&gt;Keep / Improve / Retire&lt;/code&gt; etc.&lt;/td&gt;
&lt;td&gt;"Total is 12 points, so it passes"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ No aggregation&lt;/td&gt;
&lt;td&gt;Have the No answers &lt;strong&gt;listed as the grounds&lt;/strong&gt; for the verdict&lt;/td&gt;
&lt;td&gt;Using the Yes ratio (satisfaction rate) as a quality metric&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ① Make the checks binary Yes/No
&lt;/h3&gt;

&lt;p&gt;Instead of "how specific is this skill, out of 5?", ask "is there a concrete command example you can run immediately after reading? Yes/No."&lt;/p&gt;

&lt;p&gt;Binary questions have two properties that numeric scoring lacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verifiable&lt;/strong&gt;: "Is there a command example?" can be settled black-and-white by looking at the text. "Specificity: 3 points" cannot be verified at all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt;: a No answer points directly at &lt;em&gt;what is missing&lt;/em&gt;. It feeds straight into an improvement list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the skill audit I run in production, every target goes through these four questions:&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="p"&gt;-&lt;/span&gt; [ ] Actionability: are there steps, commands, or worked examples you can run immediately?
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Scope fit: do the name, trigger, and body agree (not too broad / too narrow)?
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Uniqueness: does it avoid overlapping with other skills in the same batch?
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Currency: do the referenced paths, CLI flags, and URLs still exist today?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ② Have the verdict picked from named labels, as a holistic judgment
&lt;/h3&gt;

&lt;p&gt;Holistic judgment is what LLMs are good at. As I wrote in the previous article, LLMs are bad at scoring dimensions independently — they get pulled by the overall impression. So it's more honest to design for that from the start: "pick one, based on the overall impression."&lt;/p&gt;

&lt;p&gt;The key is to name the verdict labels so they map &lt;strong&gt;1:1 to the next action&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Verdict options&lt;/th&gt;
&lt;th&gt;Corresponding action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Skill audit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Keep / Improve / Update / Retire / Merge into [X]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep / hand off to the improvement engine / technical refresh / delete / consolidate (with an explicit target)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge-extraction gate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Save / Improve then Save / Absorb into [X] / Drop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Save / fix then save / append to an existing skill / discard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;fixed set of options&lt;/strong&gt; that downstream processing can branch on directly is more practical than a bare &lt;code&gt;accept / reject&lt;/code&gt; (in programming terms, an enum — a mechanism that forbids any value outside the defined set. With free-form text you get drift like "Improve" vs. "improve slightly", and code can't branch on it). Designs like &lt;code&gt;Merge into [X]&lt;/code&gt; that &lt;strong&gt;force a concrete target name&lt;/strong&gt; make vague verdicts like "this feels like it overlaps with something" structurally impossible to write.&lt;/p&gt;

&lt;p&gt;This separation — "the LLM picks from named options, code executes" — also acts as a firewall against judgment errors. If judging and executing are separate, an LLM's bad call can never corrupt system state directly (I run this as a pattern I call &lt;strong&gt;LLM judge + Code enforce&lt;/strong&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ Never aggregate the binary answers
&lt;/h3&gt;

&lt;p&gt;This is the pattern's biggest fork in the road. Once you have binary checks, you'll be tempted to say "6 of 8 questions are Yes, that's 75%, above the 70% threshold, so it passes." &lt;strong&gt;Don't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reason is simple: &lt;strong&gt;a satisfaction rate dilutes a single fatal No.&lt;/strong&gt; If one No says "the referenced file doesn't exist," the skill is broken even if the other seven answers are Yes. Averages and ratios convert that dominant No into a 12.5% deduction.&lt;/p&gt;

&lt;p&gt;Do these two things instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Require the No answers to be &lt;strong&gt;listed as the grounds&lt;/strong&gt; for the verdict (hidden Nos breed verdict drift)&lt;/li&gt;
&lt;li&gt;If there is even one dominant No (a nonexistent reference, an unsupported claim, etc.), tip the verdict to the non-Keep side even if everything else is Yes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The skill-audit definition file states the principle in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Evaluation is &lt;strong&gt;holistic judgment, not a numeric rubric&lt;/strong&gt; — binary answers are evidence feeding the verdict, never aggregated into a score (a satisfaction ratio changes no decision and dilutes a single dominant No).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The verdict pressure-test — attack the verdict before you commit it
&lt;/h2&gt;

&lt;p&gt;Binary checks plus holistic judgment already work, but one more quality device raises the verdict's reliability: &lt;strong&gt;before finalizing a provisional verdict, have the LLM itself generate and answer questions that try to refute it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Procedure (applied only to non-Keep candidates):
&lt;span class="p"&gt;1.&lt;/span&gt; Generate 1–3 Yes/No questions that would refute the provisional verdict
   e.g. if the provisional verdict is "Update (a referenced CLI flag is deprecated)"
   → "Doesn't that flag still exist in the current version's --help? Verify empirically"
&lt;span class="p"&gt;2.&lt;/span&gt; Answer each question with one line of evidence (file read, path check, web search result)
&lt;span class="p"&gt;3.&lt;/span&gt; Refutation succeeds (e.g. the flag still exists) → move the verdict back toward Keep
&lt;span class="p"&gt;4.&lt;/span&gt; Defect confirmed (e.g. the flag was indeed removed) → the No items from the binary
   checks become the improvement list as-is
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two disciplines govern question design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity&lt;/strong&gt;: each question tests exactly one verifiable claim&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refutation-oriented&lt;/strong&gt;: frame questions to seek refutation of the provisional verdict, not confirmation of it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more discipline matters for re-judging after fixes: &lt;strong&gt;re-judge exactly once, with the same question set.&lt;/strong&gt; If you rewrite the questions, you can no longer tell whether the fix worked or the bar just moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  A copy-paste judge prompt structure
&lt;/h2&gt;

&lt;p&gt;Here is the whole pattern in a form you can reuse by swapping in your own evaluation target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are a quality evaluator of {kind of evaluation target}. Evaluate using the following steps.

&lt;span class="gu"&gt;## Step 1: Binary checks (all questions mandatory)&lt;/span&gt;
Answer each question with Yes/No plus one line of evidence.
&lt;span class="p"&gt;-&lt;/span&gt; Q1: {verifiable question, e.g. is there a runnable code example?}
&lt;span class="p"&gt;-&lt;/span&gt; Q2: {verifiable question}
&lt;span class="p"&gt;-&lt;/span&gt; Q3: {verifiable question}
&lt;span class="p"&gt;-&lt;/span&gt; Q4: {verifiable question}

&lt;span class="gu"&gt;## Step 2: Target-specific refutation questions (only if the provisional verdict leans non-pass)&lt;/span&gt;
Pick one provisional verdict, generate 1–3 Yes/No questions that would refute it,
and answer each with one line of evidence.

&lt;span class="gu"&gt;## Step 3: Verdict&lt;/span&gt;
Choose exactly one of the following. Do not output scores or points.
&lt;span class="p"&gt;-&lt;/span&gt; {verdict_label_1}: {meaning and next action}
&lt;span class="p"&gt;-&lt;/span&gt; {verdict_label_2}: {meaning and next action}
&lt;span class="p"&gt;-&lt;/span&gt; {verdict_label_3}: {meaning and next action}

In the grounds for the verdict, list every question answered No.
If there is even one dominant No ({domain-specific fatal condition}),
choose {non-pass verdict label} even if everything else is Yes.

&lt;span class="gu"&gt;## Output format&lt;/span&gt;
Follow the JSON schema below and output &lt;span class="gs"&gt;**JSON only**&lt;/span&gt;
(no explanatory text or Markdown before or after).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Receive the output as JSON and downstream code can process it directly.&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;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Improve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&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;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is there a runnable code example?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Steps are bullet points only; not a single command example"&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;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Do the referenced paths exist?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Yes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"All 3 paths confirmed with ls"&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;"pressure_test"&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;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does the refutation hold that the bullet-point steps alone are reproducible?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The argument spec in step 3 is ambiguous; not reproducible"&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;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Actionability No is dominant. Adding a worked example to step 3 would make this Keep-worthy"&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;Since the &lt;code&gt;verdict&lt;/code&gt; field only takes fixed options, the receiving code just branches with a &lt;code&gt;switch&lt;/code&gt; statement. The No items in &lt;code&gt;evidence&lt;/code&gt; can be handed to the next stage (a human or another LLM) as an improvement task list as-is.&lt;/p&gt;

&lt;p&gt;The full design pattern, including this template, is published as a Claude Code Agent Skill at &lt;a href="https://github.com/shimo4228/llm-as-judge" rel="noopener noreferrer"&gt;github.com/shimo4228/llm-as-judge&lt;/a&gt;. Copy &lt;code&gt;skills/llm-as-judge&lt;/code&gt; into &lt;code&gt;~/.claude/skills/&lt;/code&gt; and it fires automatically whenever you're designing a judge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same principles, different operation at different scales
&lt;/h2&gt;

&lt;p&gt;Running this pattern at two scales taught me that &lt;strong&gt;the principles are shared, but the application conditions change with scale&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Knowledge-extraction gate (learn-eval)&lt;/th&gt;
&lt;th&gt;Skill audit (skill-stocktake)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation target&lt;/td&gt;
&lt;td&gt;1 draft extracted from a session&lt;/td&gt;
&lt;td&gt;The entire library (73 skills)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refutation question generation&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Unconditional&lt;/strong&gt; (3–5 questions every time)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Non-Keep candidates only&lt;/strong&gt; (1–3 questions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reason&lt;/td&gt;
&lt;td&gt;N=1, so the cost is negligible&lt;/td&gt;
&lt;td&gt;Generating for all 73 items wastes most of the shots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With a single evaluation target, go all-in — it costs nothing. At tens of items, reserve the expensive device (refutation questions) for the suspicious candidates only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does single-context evaluation lose accuracy? — testing my own design
&lt;/h2&gt;

&lt;p&gt;Here begins the second half of this article. The skill audit above originally used a design of "&lt;strong&gt;load every skill into one context and evaluate&lt;/strong&gt;." The logic: a long-context model can survey everything at once, so cross-skill duplication should also be visible.&lt;/p&gt;

&lt;p&gt;Then an audit run under this design returned &lt;strong&gt;Keep for every single item&lt;/strong&gt;. Not one defect, not one duplicate pair detected. Is that "the library is healthy" or "the evaluation is spinning idle"? To find out, I re-audited the same library two ways: &lt;strong&gt;splitting it into small batches of about 10 items, each close-read by an independent agent&lt;/strong&gt;, and &lt;strong&gt;a probe dedicated solely to duplicate detection&lt;/strong&gt; (the experiment design and caveats about the numbers are in the collapsible section at the end of this chapter).&lt;/p&gt;

&lt;h3&gt;
  
  
  Results — some dimensions degrade, others don't
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What was tested&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-item defect detection (small-batch close reading)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;12 of 73 items non-Keep&lt;/strong&gt; (16%). The single context had missed all of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate detection (dedicated probe)&lt;/td&gt;
&lt;td&gt;True duplicates: &lt;strong&gt;0&lt;/strong&gt; (all 17 candidate clusters were adjacent skills with documented role separation)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Half of the 12 missed items were &lt;strong&gt;mechanically detectable defects&lt;/strong&gt;: 404 links, references to deleted files, deprecated CLI flags. The kicker: &lt;strong&gt;two files that no longer existed on disk had been given Keep verdicts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Zero duplicates, on the other hand, turned out to be reality, not dilution (attention thinning as the context fills up). The single-context rationale — "you can see duplication because you survey everything at once" — had been fully replaced by a lightweight description scan plus targeted close reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  The biggest lesson isn't context width — it's verification ownership
&lt;/h3&gt;

&lt;p&gt;Tracing the mechanism of the misses revealed a cause deeper than context width itself. The audit definition contained a conditional verification instruction: "&lt;strong&gt;if a reference looks stale, check it.&lt;/strong&gt;" That "if it looks..." condition is itself a judgment that depends on the LLM's attention. When the context fills up and attention thins, conditional triggers default to "don't check."&lt;/p&gt;

&lt;p&gt;The previous article's conclusion was "mechanical work to scripts, quality judgment to AI." This experiment redrew that boundary one level deeper: &lt;strong&gt;inside quality judgment there are deterministic parts hiding, and they belong in code.&lt;/strong&gt; "Does this file exist?" is not a judgment — it's &lt;code&gt;ls&lt;/code&gt;. The deterministic checks that had snuck into the judgment phase should have been moved to the code side as an unconditional, mandatory pre-pass. That is what "verification ownership" in the heading means: moving the execution owner of deterministic checks from the LLM's attention to code.&lt;/p&gt;

&lt;p&gt;The revised design (v3.0) assigns each property to the environment that should check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 0: Deterministic pre-pass (code-owned, unconditional)
         Reference-existence checks, ledger consistency.
         Anything that doesn't exist never reaches judgment.
Phase 1: Inventory
         (the layer offloaded to scripts in the previous article; unchanged)
Phase 2: Per-item scrutiny (small batches of ~10, parallel)
         Binary checks + verdict pressure-test + holistic judgment
Phase 3: Dedicated duplication probe (1 agent, all items)
         Lightweight description scan → close-read candidate clusters only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reviewed the 12 detections one by one and fixed them the same day. For one of them, testing the proposed fix before applying it showed the command didn't actually work, so I swapped in a different form. Verify not just the verdict but &lt;strong&gt;the fix itself before committing it&lt;/strong&gt; — the same pattern surfacing outside the evaluation loop.&lt;/p&gt;

&lt;p&gt;Experiment design and notes on the numbers (for the curious)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt;: the audit result from the all-in-one-context run. On the ledger it was 84 entries, all Keep. The actual files on disk numbered 73 — the gap was a ledger bug: the same skill double-counted under two different keys, plus entries for files that no longer existed. That the ledger was this dirty had gone unnoticed by everyone at that point. The follow-up experiments target the 73 real files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treatment A (small-batch close reading)&lt;/strong&gt;: split the 73 items into 7 batches of 10–11, each close-read by an independent subagent using the same criteria (binary checks → holistic judgment). Reference existence had to be verified with &lt;code&gt;ls&lt;/code&gt; etc., stated explicitly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treatment B (dedicated duplication probe)&lt;/strong&gt;: one agent reads all 73 descriptions, enumerates candidate clusters → close-reads the bodies side by side to decide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bias control&lt;/strong&gt;: the agents were not told that the baseline had returned all-Keep&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An honest note on confounding&lt;/strong&gt;: Treatment A's prompt explicitly required "verify reference existence with &lt;code&gt;ls&lt;/code&gt;", so part of the difference from baseline is attributable to "forced verification" rather than "context width." However, detections that required semantic judgment — description/body mismatches, overly abstract writing, scope drift — cannot be explained by forced verification, and I read those as the effect of attention density: 10 items per batch versus 73&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why no scores — the boundary the proposing paper drew itself
&lt;/h2&gt;

&lt;p&gt;The "decompose evaluation criteria into binary questions" approach has a research lineage: CheckEval (arXiv:2403.18771, 2024), TICK (arXiv:2410.03608, 2024), and the later BinEval, "Ask, Don't Judge" (arXiv:2606.27226, 2026). My setup had the "binary checks + holistic judgment" skeleton first; I learned of this lineage afterward and folded it into each skill's References. BinEval proposes decomposing into atomic Yes/No questions and wiring failed questions directly into improvement feedback — the dynamic generation of refutation questions and the "No answer = improvement list" wiring are the parts I ported from it after it came out.&lt;/p&gt;

&lt;p&gt;What's interesting is BinEval's own limitations section. The paper self-reports two reservations. First: binary decomposition is reliable only up to &lt;strong&gt;concretely checkable criteria&lt;/strong&gt;, like "is there a code example?" Subjective quality like "is this well written?" is something humans judge by looking at the whole rather than stacking individual checks — it &lt;strong&gt;cannot be fully decomposed into a bundle of Yes/No questions&lt;/strong&gt; (less reliable). Second: "it assumes &lt;strong&gt;the fraction of satisfied questions corresponds roughly linearly to quality&lt;/strong&gt;, and &lt;strong&gt;this does not always hold&lt;/strong&gt;." In other words, using the satisfaction rate of decomposed questions as a score comes with a caveat from the proposing paper itself.&lt;/p&gt;

&lt;p&gt;Flip that limitations report around and it is exactly a &lt;strong&gt;division-of-labor boundary&lt;/strong&gt;. Concretely checkable criteria can be decomposed into Yes/No — so let the checks gather evidence. The final value judgment belongs to the subjective "is this good writing?" layer, which reduces neither to a bundle of questions nor to a satisfaction rate — so that layer stays a holistic judgment. The three principles in this article are that self-reported boundary, transcribed directly into a design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Design LLM-as-judge evaluation around three principles: "binary checks = evidence," "verdict = pick one named label holistically," "no aggregation"&lt;/li&gt;
&lt;li&gt;Make verdict labels a fixed set mapping 1:1 to next actions, and require every No answer to be listed as grounds for the verdict&lt;/li&gt;
&lt;li&gt;A verdict pressure-test before finalizing raises reliability. Re-judge exactly once, with the same question set&lt;/li&gt;
&lt;li&gt;In my experiment, per-item scrutiny diluted as the context filled up (16% missed). Don't put deterministically verifiable checks on the LLM's attention — move them to unconditional, code-owned execution&lt;/li&gt;
&lt;li&gt;The reason not to turn "satisfaction rate" into a score comes from the limitations self-reported by the binary-decomposition paper itself (BinEval): decomposition works up to concrete criteria; the value-judgment layer stays holistic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The previous article's division of labor — "mechanical work to scripts, quality judgment to AI" — got one level deeper this time. &lt;strong&gt;Inside judgment, too, there are parts that belong in code.&lt;/strong&gt; When you design an LLM-as-judge, start by asking, before anything reaches the judge: "is this really a judgment, or is it &lt;code&gt;ls&lt;/code&gt;?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/llm-as-judge" rel="noopener noreferrer"&gt;llm-as-judge&lt;/a&gt; — the design pattern from this article, packaged as an Agent Skill (install and use as-is)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shimo4228/offloading-ais-weak-spots-to-shell-scripts-designing-building-and-publishing-a-skill-audit-2ll8"&gt;Previous article: Offloading AI's Weak Spots to Shell Scripts — Designing, Building, and Publishing a Skill Audit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/claude-skill-stocktake" rel="noopener noreferrer"&gt;claude-skill-stocktake&lt;/a&gt; — the skill-audit command from this article (v3.0 hybrid structure)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/agent-knowledge-cycle" rel="noopener noreferrer"&gt;agent-knowledge-cycle&lt;/a&gt; — where the LLM judge + Code enforce pattern mentioned in this article is defined&lt;/li&gt;
&lt;li&gt;Paper: &lt;a href="https://arxiv.org/abs/2403.18771" rel="noopener noreferrer"&gt;CheckEval&lt;/a&gt; (arXiv:2403.18771, 2024) — prior work on checklist-decomposition LLM evaluation&lt;/li&gt;
&lt;li&gt;Paper: &lt;a href="https://arxiv.org/abs/2410.03608" rel="noopener noreferrer"&gt;TICK&lt;/a&gt; (arXiv:2410.03608, 2024) — evaluation and generation improvement via generated checklists&lt;/li&gt;
&lt;li&gt;Paper: &lt;a href="https://arxiv.org/abs/2606.27226" rel="noopener noreferrer"&gt;BinEval, "Ask, Don't Judge"&lt;/a&gt; (arXiv:2606.27226, 2026) — source of the atomic Yes/No decomposition and "No answer = improvement list" wiring; its limitations section grounds the boundary drawn in this article&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt; — the author's other repositories&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>promptengineering</category>
      <category>evaluation</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Building an Autonomous Agent on an M1 Mac, by Choice</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:00:06 +0000</pubDate>
      <link>https://dev.to/shimo4228/building-an-autonomous-agent-on-an-m1-mac-by-choice-5b5o</link>
      <guid>https://dev.to/shimo4228/building-an-autonomous-agent-on-an-m1-mac-by-choice-5b5o</guid>
      <description>&lt;p&gt;For about 3 months I've been running an autonomous agent — one that thinks up and writes its own social media posts and comments — unattended, 4 sessions daily, on a 16GB M1 Mac with small models in the 9B / E4B class. I'm about to publish what that operation taught me about hardening, as a series of 4 technical articles.&lt;/p&gt;

&lt;p&gt;Before that, there's one thing I want to write down first: &lt;strong&gt;why small models&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I've been to the purchase page for a Mac Studio or a new MacBook Pro more than once or twice. Backing the agent with a large cloud model (Opus or the GPT family) has always been an option in the code. And yet I haven't bought, and I haven't switched. The 16GB M1 is not an economic constraint — it's a &lt;strong&gt;constraint I chose&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the outside, building on small models looks like a cheap compromise. This article explains why it isn't, and states where I stand. It also serves as the hub for the 4-article series.&lt;/p&gt;

&lt;h2&gt;
  
  
  A model's intelligence hides the roughness of your design
&lt;/h2&gt;

&lt;p&gt;Large models absorb sloppy prompts, ambiguous instructions, and missing guards with sheer intelligence. If all you want is to ship a product, that's a virtue. But if you want to &lt;strong&gt;become someone who can build things&lt;/strong&gt;, it becomes a defect.&lt;/p&gt;

&lt;p&gt;Because inside the thing that worked, you can no longer tell where your design ends and the model's intelligence begins. "It worked" and "I built it" are different things. Something you bludgeoned into working with model capability counts as a thing that ran — it doesn't become the ability to build.&lt;/p&gt;

&lt;p&gt;Small models have no absorption capacity. So every design flaw comes to the surface.&lt;/p&gt;

&lt;p&gt;In my operation, all of the following surfaced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The context window being silently truncated&lt;/li&gt;
&lt;li&gt;Outputs cut off midway&lt;/li&gt;
&lt;li&gt;A runaway caused by one missing sampling parameter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In cloud or large-model environments, these rarely bother you. The environment has cushioning built in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context windows are in the 200K–1M token class, so truncation itself rarely happens. And when you do exceed the limit, you get an explicit error rather than a silent cut&lt;/li&gt;
&lt;li&gt;Sampling defaults are managed on the API side; there is structurally no room to drop a parameter&lt;/li&gt;
&lt;li&gt;Even when a slightly-off output slips through, a smart model running the ReAct pattern (where the LLM looks at each result and picks the next move) recovers on the next step and rounds the loop off plausibly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A local 16GB machine has none of this cushioning. You specify the context window yourself, overflow is silently truncated from the head, and the sampling settings are your own property. The same class of problems surfaced in a form that could not be hidden, and the only way to close them was design.&lt;/p&gt;

&lt;p&gt;As long as you're running on cushioning, you can't tell whether your design is protecting you or the environment is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training under constraints outlasts the environment
&lt;/h2&gt;

&lt;p&gt;There was a period when I did business automation in an environment where I couldn't freely bring in new tools. Security constraints, constraints on approved software, and organizational friction against the very act of "introducing something new." What I could use was software already installed on everyone's PC — Excel and Access — plus an approved RPA tool.&lt;/p&gt;

&lt;p&gt;If you can't add tools, the only way forward is to know the combinations of the tools at hand, deeply. Within those limits, I pushed automation as far as the available tools would go. But what that experience really trained wasn't tool knowledge.&lt;/p&gt;

&lt;p&gt;The substance of automation is decomposing work that had been running on personal, artisanal tacit knowledge. Up to which step can be written deterministically, and from where does it require holistic semantic judgment grounded in experience?&lt;/p&gt;

&lt;p&gt;You examine that boundary as finely as you possibly can, and sort the work into &lt;strong&gt;deterministic parts and parts that need semantic judgment&lt;/strong&gt;. Only the former may be handed to RPA; automation that misjudges the boundary always breaks.&lt;/p&gt;

&lt;p&gt;That sorting maps directly onto how I design agents today.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Then&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Routine processing writable in Excel / Access / RPA&lt;/td&gt;
&lt;td&gt;Scripts and code (deterministic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parts needing human semantic judgment&lt;/td&gt;
&lt;td&gt;LLM (semantic judgment, probabilistic generation)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I don't throw deterministic work at the LLM&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. I use the LLM only where semantic judgment is required. And since an LLM is not a substitute for human judgment, output verification and final decisions stay on the code-and-human side.&lt;/p&gt;

&lt;p&gt;Deciding which side a task belongs to — that judgment is the center of the design. It was trained in a constraint-riddled environment, and it has stayed with me as a principle after the constraints disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  A design that runs on small models doesn't depend on the model
&lt;/h2&gt;

&lt;p&gt;The small-model constraint has one more meaning beyond training: &lt;strong&gt;proof of a lower bound&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A design that runs unattended for 3 months on the 9B / E4B class is not plugging design holes with model intelligence. Semantic judgment remains the LLM's job, but every protective mechanism lives on the code side.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inputs are estimated and guarded before sending&lt;/li&gt;
&lt;li&gt;Output truncation is detected and discarded&lt;/li&gt;
&lt;li&gt;Failures are structurally recorded by telemetry (the machinery that records the system's behavior)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are model-independent designs. So the design thinking carries over intact if you migrate to a large model. What doesn't carry over are the calibration values and thresholds tied to a specific model (that migration problem is what series #3 covers).&lt;/p&gt;

&lt;p&gt;The reverse doesn't hold. A design built on the assumption of a large model's intelligence won't run as-is in a smaller environment. Stacking upward is easy; stepping downward requires a rebuild.&lt;/p&gt;

&lt;p&gt;Of course, with a large model the same thing would have run with far less effort. But with something that ran easily, you never find out where design ends and model intelligence begins. Whatever ran on the small model, I can call my own design. That is what I mean when I say this is not a compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the series covers technically
&lt;/h2&gt;

&lt;p&gt;That was the "why small" part. The techniques that actually made unattended operation work on 16GB are covered in the 4 articles that follow (links will be added as they're published).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local LLM failures happen quietly&lt;/strong&gt; — diagnosing silent truncation of inputs, and designing a budget guard that protects before sending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The day I dropped a 1.8x faster backend&lt;/strong&gt; — adopting MLX because it won the benchmarks, and retreating after 24 hours in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The day you swap the model&lt;/strong&gt; — blind A/B testing, and the migration problem of model-tied calibration values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't fix what you can't observe&lt;/strong&gt; — how telemetry, replay, and tests quietly lie&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The build log of this agent itself starts at &lt;a href="https://zenn.dev/shimo4228/articles/moltbook-agent-scratch-build" rel="noopener noreferrer"&gt;Building an agent from scratch — Claude Code and security-first development&lt;/a&gt; (in Japanese). The episode where the fight with small models began in earnest is &lt;a href="https://zenn.dev/shimo4228/articles/few-shot-for-small-models" rel="noopener noreferrer"&gt;My agent's memory broke — a day wrestling a 9B model&lt;/a&gt; (in Japanese).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Choosing small models is not an economic compromise — it's a &lt;strong&gt;constraint chosen to expose design flaws&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;What constraints train is not tool knowledge but the &lt;strong&gt;ability to sort deterministic parts from parts that need semantic judgment&lt;/strong&gt;. That sorting carries straight over to the code/LLM division of labor at the center of agent design&lt;/li&gt;
&lt;li&gt;The design thinking that sustains unattended operation on small models is model-independent and carries over intact to larger models (only the calibration values need re-measuring)&lt;/li&gt;
&lt;li&gt;Whether to stay or move depends on your goal. If you want something running as fast as possible, cloud is the better fit. If you want the ability to build, small models are worth staying with&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Contemplative Agent — the autonomous agent operated in this article (repo with public ADRs and evidence): &lt;a href="https://github.com/shimo4228/contemplative-agent" rel="noopener noreferrer"&gt;https://github.com/shimo4228/contemplative-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent Skill for deciding "deterministic code or LLM": &lt;a href="https://github.com/shimo4228/when-code-when-llm" rel="noopener noreferrer"&gt;https://github.com/shimo4228/when-code-when-llm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent Skill collecting design patterns for layering code and LLMs: &lt;a href="https://github.com/shimo4228/code-and-llm-collaboration" rel="noopener noreferrer"&gt;https://github.com/shimo4228/code-and-llm-collaboration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Which business tasks actually need a ReAct agent (in Japanese): &lt;a href="https://zenn.dev/shimo4228/articles/react-agent-business-quadrant" rel="noopener noreferrer"&gt;https://zenn.dev/shimo4228/articles/react-agent-business-quadrant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Where the build log starts (in Japanese): &lt;a href="https://zenn.dev/shimo4228/articles/moltbook-agent-scratch-build" rel="noopener noreferrer"&gt;https://zenn.dev/shimo4228/articles/moltbook-agent-scratch-build&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The previous round of the fight with small models (in Japanese): &lt;a href="https://zenn.dev/shimo4228/articles/few-shot-for-small-models" rel="noopener noreferrer"&gt;https://zenn.dev/shimo4228/articles/few-shot-for-small-models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Author's GitHub: &lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;https://github.com/shimo4228&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Concretely, this means I don't default to the ReAct pattern, which hands loop control and tool selection to the LLM. Code owns the flow of processing, and the LLM is called only at the spots where semantic judgment is its assigned job. Which business tasks genuinely require ReAct is covered in "&lt;a href="https://zenn.dev/shimo4228/articles/react-agent-business-quadrant" rel="noopener noreferrer"&gt;Which business tasks actually need a ReAct agent&lt;/a&gt;" (in Japanese).&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>discuss</category>
      <category>ollama</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Why Did My Agent Decide That? 3 Observability Patterns</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:00:08 +0000</pubDate>
      <link>https://dev.to/shimo4228/why-did-my-agent-decide-that-3-observability-patterns-ami</link>
      <guid>https://dev.to/shimo4228/why-did-my-agent-decide-that-3-observability-patterns-ami</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: three design patterns that make an autonomous AI agent's decision-making "reconstructable after the fact" (replayable audit logs / read-only instruments / shadow-mode validation), and how to install them into your own agent as Agent Skills.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The three walls these patterns address
&lt;/h2&gt;

&lt;p&gt;Once you build an AI agent and start running it, you hit a series of problems that are a different species from feature work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can't explain the "why" of a decision after the fact.&lt;/strong&gt; Someone reports odd behavior, but nothing was kept that lets you reconstruct what the LLM or the heuristic saw and how it decided at that moment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The output feels like it's drifting" can't justify an intervention.&lt;/strong&gt; You have the gut feeling but no numbers, so you tweak settings on intuition and can't even tell whether the tweak worked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wiring a new LLM decision mechanism into production is scary.&lt;/strong&gt; If the selector or classifier is wrong, the output just degrades quietly — and by the time you notice, the old behavior you'd want as a baseline is gone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are, at bottom, a lack of observability — the property that a system's internal state can be reconstructed from the outside. As AI agents move into production, interest in telemetry (the machinery by which a system records and emits data about its own behavior) via standards like OpenTelemetry is growing too. But what traces and metrics tell you by default is "what the request did" — they don't reach the three walls above.&lt;/p&gt;

&lt;p&gt;I've published the design patterns that address each wall as Agent Skills, in the same form I use to operate an autonomous agent&lt;sup id="fnref1"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/shimo4228/agent-observability-patterns" rel="noopener noreferrer"&gt;https://github.com/shimo4228/agent-observability-patterns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each pattern, I'll include one concrete thing it solved &lt;strong&gt;in my own operation&lt;/strong&gt;. The shared stance fits in one line.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Observation precedes intervention&lt;/strong&gt; — ship the logs before the failure. Read the instruments before intervening. Let a new mechanism earn its evidence in shadow before wiring it into production.&lt;br&gt;
"Shadow" is not a coinage of mine. It comes from &lt;strong&gt;shadow deployment / dark launch&lt;/strong&gt;, an established deployment technique in ML and service operations — route production traffic to the new system too, record its outputs, but never serve them — and here it means running a candidate mechanism observe-only, in parallel. In this article I call that shadow mode (details in Pattern 3).&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;The patterns themselves are Markdown design documents, so they're readable regardless of language or framework&lt;/li&gt;
&lt;li&gt;To load them into an agent as skills, you need an &lt;a href="https://agentskills.io/specification" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt;-compatible agent such as Claude Code&lt;/li&gt;
&lt;li&gt;The repository is agent-observability-patterns linked above (MIT license, v0.1.0)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The big picture: three patterns layered on the same record
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wall&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;How it solves it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can't explain the "why" of a decision after the fact&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/shimo4228/agent-observability-patterns/blob/main/skills/replayable-audit-logs/SKILL.md" rel="noopener noreferrer"&gt;replayable-audit-logs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ship a replayable audit log &lt;strong&gt;in the same change&lt;/strong&gt; as the feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A felt drift can't justify an intervention&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/shimo4228/agent-observability-patterns/blob/main/skills/read-only-instruments/SKILL.md" rel="noopener noreferrer"&gt;read-only-instruments&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Hand the operator read-only aggregates over accumulated state &lt;strong&gt;before intervening&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wiring in a new decision mechanism is scary&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/shimo4228/agent-observability-patterns/blob/main/skills/shadow-mode-validation/SKILL.md" rel="noopener noreferrer"&gt;shadow-mode-validation&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Run the candidate observe-only in parallel; &lt;strong&gt;the accumulated record&lt;/strong&gt; decides adoption&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three are not separate techniques — they stack as layers on the same record. The logs are the base corpus, the instruments are lenses you place on top of it, and shadow mode is the discipline of "building that corpus for a mechanism that isn't yet allowed to act."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    S[Shadow-mode candidate mechanism&amp;lt;br/&amp;gt;records what it would have chosen] --&amp;gt; A
    A[Replayable audit logs&amp;lt;br/&amp;gt;event-level corpus] --&amp;gt; B[Read-only instruments&amp;lt;br/&amp;gt;aggregate readings over the corpus]
    B --&amp;gt; O[Operator decides&amp;lt;br/&amp;gt;interventions and adoption]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take the patterns one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Ship the logs before the failure
&lt;/h2&gt;

&lt;p&gt;The first wall is what audit logs are for.&lt;/p&gt;

&lt;p&gt;The audit log (audit trail) itself is an established term from security and compliance: an append-only record of "who did what, when" — AWS CloudTrail and database audit logs are the canonical examples. This pattern applies that idea to an agent's decisions, and — in the lineage of event sourcing (record what happened as an immutable append-only sequence, and do all later analysis as reads over that sequence) — additionally demands that the record be &lt;em&gt;replayable&lt;/em&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ordinary logs&lt;/th&gt;
&lt;th&gt;Audit logs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reader&lt;/td&gt;
&lt;td&gt;Humans (during debugging)&lt;/td&gt;
&lt;td&gt;Machines (replaying decisions later)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Free-form messages&lt;/td&gt;
&lt;td&gt;One decision = one record, machine-readable (JSONL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What's kept&lt;/td&gt;
&lt;td&gt;A description of what happened&lt;/td&gt;
&lt;td&gt;The exact input the decision saw + the decision's outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When added&lt;/td&gt;
&lt;td&gt;After an investigation starts&lt;/td&gt;
&lt;td&gt;In the same change that adds the feature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row &lt;em&gt;is&lt;/em&gt; the principle. &lt;strong&gt;Any feature with external I/O, LLM calls, or heuristic decisions ships its audit log in the same change that adds the feature.&lt;/strong&gt; The corpus (the accumulated record) must exist before the failure it will one day explain.&lt;/p&gt;

&lt;p&gt;By "heuristic decision" I mean a branch that is decided by rules in code but whose correct answer isn't self-evident — "if the regex parse fails, fall back to the LLM," "adopt only the items whose score clears the threshold." These branches get asked "why did that happen?" after the fact, just like external I/O and LLM calls do.&lt;/p&gt;

&lt;p&gt;What this solved in my operation was repairing a broken parser. A feature that submits generated answers to an external verification system had been logging every attempt as an ordinary side effect. Weeks later, when the parser needed repair, I could replay the hundreds of real-traffic records left in the log offline, and validate the fix against a hard gate: "zero errors against known correct and incorrect answers." Fixing it against real data rather than synthetic test cases was only possible because the log existed before the failure. A log hastily added after the investigation starts can't do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Record design: not "readable" — "replayable"
&lt;/h3&gt;

&lt;p&gt;The design goal of the log is not "readable later" but "replayable offline."&lt;/p&gt;

&lt;p&gt;"Replay" is not a metaphor. Because the exact input the decision saw is in the log, you can feed that input back through today's code and diff the result against the recorded outcome. A merely readable log tells you "the parse failed," but without the input you can't run this re-verification.&lt;/p&gt;

&lt;p&gt;"Offline" means the re-run needs no external system, no LLM, and no network. Only the deterministic layers — parsers, rules — get re-executed (the LLM's response is preserved in the record as the raw output from that time), so you can run it locally as many times as you want. That makes replay fast, free, and reproducible — good enough to sit as a pass/fail gate in CI.&lt;/p&gt;

&lt;p&gt;In my operation, this replay exists as a single dedicated script. It imports the production parser function as-is, re-applies it to every unique input in the audit log, checks the results against "records the external system accepted = known correct, records it rejected = known incorrect," and returns a zero-errors pass/fail via exit code — pure code. Run it after every parser fix and you have machine-checked proof that "no previously-correct case was broken."&lt;/p&gt;

&lt;p&gt;The record checklist that makes this possible has five items.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw-input recoverability&lt;/td&gt;
&lt;td&gt;The exact input the decision saw. Store untrusted text (API responses, user prose) as &lt;strong&gt;base64 + sha256&lt;/strong&gt;, never as raw text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision path&lt;/td&gt;
&lt;td&gt;Which branch, which layer handled it (e.g. &lt;code&gt;code_parse / llm_extract / llm_reason / none&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reason code&lt;/td&gt;
&lt;td&gt;Abstentions, fallbacks, and failures get machine-aggregatable category codes (null when the decision succeeded normally). &lt;strong&gt;A silent fallback is a defect&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;What happened downstream (accepted / rejected / error) + a sanitized error message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp + stable key&lt;/td&gt;
&lt;td&gt;ISO timestamp and a hash of the input, so the same input can be matched across retries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single record looks like this (append-only JSONL; &lt;code&gt;input_sha256&lt;/code&gt; is abbreviated here for print, but in practice it holds the full 64-hex-digit value).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"ts": "2026-07-11T02:14:07Z", "input_b64": "PGV4dGVybmFsPuKApg==", "input_sha256": "95cbc571…cba8c", "truncated": false, "decision_path": "llm_extract", "reason_code": null, "outcome": "rejected", "error": null}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Base64-encoding untrusted text closes an accidental prompt-injection path (a string in a log acting as an instruction to a downstream LLM): the moment someone opens the log, an attack string would otherwise hit eyes and downstream processing as raw text. But &lt;strong&gt;base64 is encoding, not sanitization.&lt;/strong&gt; The decoded content must be treated as untrusted input all over again, and since an LLM can decode base64 by itself, an encoded log does not become "safe data" you may feed to an LLM.&lt;/p&gt;

&lt;p&gt;This may look like overkill. But the agent in question operates on Moltbook, a social network where AI agents post to each other, and most of the text its decisions read is "input written by other agents and the humans behind them — anything could be planted in it." In an environment where prompt injection must be assumed, discipline at the moment of logging becomes part of the defense line itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code-review question
&lt;/h3&gt;

&lt;p&gt;The pattern compresses into a single review question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When this feature misbehaves, which log explains the "why"? Can that behavior be replayed offline from the log?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If there's no answer, you go back to design within the same change. "We'll add logging later" violates the very principle of having the corpus before the failure.&lt;/p&gt;

&lt;p&gt;Real logs that survive this question include: a verification audit log recording each solver attempt's decision, a drift log tracking changes in an API's request/response shapes, an approval gate's accept/reject records, and caller-tagged LLM telemetry (measured failure rates per calling feature and per reason — the kind of evidence that can justify a prompt change).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Instruments are for reading, not for wiring into behavior
&lt;/h2&gt;

&lt;p&gt;The second wall is what instruments are for. "Instrument" here in the aircraft-cockpit sense: a gauge you read, not a lever you pull. Instruments are read-only aggregates over the agent's accumulated state (corpus, memory, logs) — distributions, composition ratios, cluster structure — so the operator can choose interventions from data instead of intuition.&lt;/p&gt;

&lt;p&gt;What this solved in my operation was quantifying the feeling that "the agent's memory keeps accumulating the same story." The agent from Pattern 1 distills observations from its Moltbook activity (posts, comments, replies) into patterns and keeps appending them to memory (a text corpus). When the patterns converge on the same voice, the downstream generation that reads them converges too — the echo-chamber shape. So I added a module reporting "supply per consumed category" and "the distribution of cosine similarity across the whole pool," making rebalance-or-not a decision made by reading, not by feel.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to build one — and when to remove one
&lt;/h3&gt;

&lt;p&gt;There is exactly one criterion for building: &lt;strong&gt;build the instrument only when its reading would change a specific, nameable action.&lt;/strong&gt; Instruments built because "it might be nice to have" get read by no one.&lt;/p&gt;

&lt;p&gt;The interesting part is that the same criterion drives removal. An instrument whose reading is constant under the current pipeline changes no action, so it comes off the panel. Sunk cost — the fact that you built it — is not a reason to keep a gauge on the instrument panel.&lt;/p&gt;

&lt;p&gt;This too actually happened in my operation. I shipped an instrument reading the composition of the agent's memory supply (self-reflection-derived vs. external-reply-derived), and the reading came back "100% self-derived" — a constant, every time. The instrument wasn't broken; the pipeline's structure simply almost never generated externally-derived records. With no decision consuming the reading, the answer is deletion, not repair. I removed it the same day it shipped, and preserved the one fact it taught (that the memory supply was entirely inward-facing) as prose in the design record.&lt;/p&gt;

&lt;h3&gt;
  
  
  The invariant that matters most: never wire readings into behavior
&lt;/h3&gt;

&lt;p&gt;Instrument readings go to the operator; they do &lt;strong&gt;not&lt;/strong&gt; flow directly into gates, ranking, retrieval, or promotion decisions. An operator reading a gauge and re-deciding a threshold is the intended loop. The moment code starts consuming the number at runtime, it's no longer an instrument — it's an intervention, and outside this pattern's scope.&lt;/p&gt;

&lt;p&gt;If your readings are embedding-based, calibrate with three anchors, because an unanchored number means nothing (the values below are measurements from one operation — re-measure in your own environment).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anchor&lt;/th&gt;
&lt;th&gt;How to measure&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;Floor (what even unrelated text scores)&lt;/td&gt;
&lt;td&gt;Cosine similarity between deliberately unrelated text and the corpus&lt;/td&gt;
&lt;td&gt;~0.33–0.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus mean&lt;/td&gt;
&lt;td&gt;Pairwise mean across the whole pool&lt;/td&gt;
&lt;td&gt;~0.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ceiling band (the best you see in practice)&lt;/td&gt;
&lt;td&gt;Best match within a consumed category&lt;/td&gt;
&lt;td&gt;~0.68–0.77&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this example, the echo signal was "the corpus mean sitting 0.1–0.2 above the floor." But a single-domain corpus naturally sits above the floor simply because its topics are similar, so that gap alone doesn't decide anything. Read it against your corpus's history and qualitative samples. And whenever the geometry changes — swapping the embedding model, rewriting the seeds — re-measure the scale.&lt;/p&gt;

&lt;p&gt;Misreading traps (these bite after you've built the instrument)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A flat reading does not mean "nothing changed."&lt;/strong&gt; The pairwise mean can hold steady while the corpus visibly changes character (the change was orthogonal to the instrument's axis). Corroborate a flat reading with qualitative samples before concluding "no effect"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores of gate survivors are not a distribution.&lt;/strong&gt; You cannot analyze survivor-only data as if it were the population's distribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A coarse scorer's threshold can only sit at boundaries where values actually exist.&lt;/strong&gt; With a scorer that emits a handful of discrete values, drawing a threshold between them is fiction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pattern 3: A new decision mechanism earns its evidence in shadow before it gets wired in
&lt;/h2&gt;

&lt;p&gt;The third wall is what shadow mode is for (defined at the top; the lineage is laid out in &lt;a href="https://microsoft.github.io/code-with-engineering-playbook/automated-testing/shadow-testing/" rel="noopener noreferrer"&gt;Microsoft's Engineering Playbook&lt;/a&gt;). The fear is really irreversibility: you can't know without wiring it in, and once wired in, you can't go back.&lt;/p&gt;

&lt;p&gt;Use it &lt;strong&gt;when the unknown is decision quality, not code correctness&lt;/strong&gt;. Unit tests can prove parsing and wiring are correct; they cannot prove "the small local model picks the right skill." When the failure mode is "plausibly wrong," the answer lives only in real traffic.&lt;/p&gt;

&lt;p&gt;The other trigger is &lt;strong&gt;when the wiring is a one-way door&lt;/strong&gt;. Connecting the mechanism changes what the model reads and emits, so if the mechanism is bad, output degrades quietly. Delete the old path and you've also deleted the baseline you'd need to notice the degradation. Shadow-first keeps the baseline alive while the evidence accumulates.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real case from my operation: narrowing skill injection
&lt;/h3&gt;

&lt;p&gt;The autonomous agent I run (the one from Pattern 1 — it posts, comments, and replies on Moltbook automatically) injects its 19 learned skill documents — about 20k tokens — in full into the system prompt on every generation, filling about 60% of the context window with skills. I wanted a small local LLM to select "only the skills that apply to the current situation." But the small model's selection accuracy was an unknown, and wiring it in is a one-way door.&lt;/p&gt;

&lt;p&gt;So I mounted the selection mechanism in shadow mode. Injection stays full-text as before; only the "what it would have chosen" records accumulate, and adoption is decided after 2–4 weeks of hallucination-rate and reduction numbers.&lt;/p&gt;

&lt;p&gt;In review of this shadow implementation, a model from a different family caught the following chain (two reviewers from the same family had missed it):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The shadow selection call goes through the &lt;strong&gt;same LLM-call infrastructure&lt;/strong&gt; as production generation. That infrastructure has one circuit breaker (a protection mechanism that halts further calls after consecutive failures) — shared with production&lt;/li&gt;
&lt;li&gt;If shadow calls fail repeatedly, the shared breaker's failure counter climbs and the circuit opens&lt;/li&gt;
&lt;li&gt;The production post-generation that runs right after sees "circuit open" and gets skipped&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shipped as-is, &lt;strong&gt;the machinery installed to observe would have silently halted the behavior it was observing.&lt;/strong&gt; And the log would have said only "generation skipped." The fix was one layer of isolation: shadow failures don't count toward the shared breaker. The lesson is generalized into element 2 of the six below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six design elements (each one paid for with a real production bruise)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Observe-only entry point, enforced by types&lt;/td&gt;
&lt;td&gt;The shadow hook returns nothing (&lt;code&gt;-&amp;gt; None&lt;/code&gt;). Callers can't consume the selection result &lt;strong&gt;even by accident&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Isolation from shared failure machinery&lt;/td&gt;
&lt;td&gt;The chain above. Audit every piece of shared state the shadow path touches: circuit breakers, rate limits, caches, retry counters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Validate probabilistic output before recording&lt;/td&gt;
&lt;td&gt;Check answers against the ground set (catalog names); keep out-of-set answers away from downstream but &lt;strong&gt;do record them&lt;/strong&gt;. Hallucination rate is first-class data for the adoption decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Bake "what it meant at the time" into the record&lt;/td&gt;
&lt;td&gt;Metrics computed against mutable state (total token counts, etc.) get written at record time. Recompute at report time and you can no longer replay "what the decision looked like then"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Kill switch = absence of config&lt;/td&gt;
&lt;td&gt;No shadow config written means the whole path is off (no LLM calls, no files). No feature flags to maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Reserve exit criteria at launch; fill in numbers later&lt;/td&gt;
&lt;td&gt;The metrics the adoption decision will read (hallucination rate, fail-open rate = fraction where the answer didn't parse and passed through untouched, etc.) and the observation window (2–4 weeks) go into the decision record at launch. But &lt;strong&gt;pass/fail numbers are not set before the data exists&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sixth may feel counterintuitive, but a threshold invented before seeing data is a guess wearing the clothes of a number. One clean run proves the wiring is correct, not that the mechanism is good. In fact, during pre-publication review of these skills, a different-family model (&lt;a href="https://dev.to/shimo4228/i-built-a-skill-for-easy-codex-reviews-from-claude-code-4h89"&gt;the review setup is described in this article&lt;/a&gt;) proposed "pre-register the pass/fail thresholds" — and since that inverts this design decision, I documented the rationale and rejected it. When a plausible general principle collides with a specific judgment you paid for in bruises, take the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install
&lt;/h2&gt;

&lt;p&gt;Clone the repository and copy the skill directories into your agent's skills location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shimo4228/agent-observability-patterns.git
&lt;span class="nb"&gt;cd &lt;/span&gt;agent-observability-patterns

&lt;span class="c"&gt;# For Claude Code&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; skills/replayable-audit-logs ~/.claude/skills/replayable-audit-logs
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; skills/read-only-instruments ~/.claude/skills/read-only-instruments
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; skills/shadow-mode-validation ~/.claude/skills/shadow-mode-validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, the agent will consult each pattern in situations like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding or reviewing a feature with external I/O, LLM calls, or heuristic decisions (→ audit logs)&lt;/li&gt;
&lt;li&gt;Turning an observation like "I think an echo is forming" into numbers before intervening (→ instruments)&lt;/li&gt;
&lt;li&gt;About to wire an unvalidated LLM decision — a selector, classifier, or gate — close to production behavior (→ shadow mode)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Observability for autonomous agents compresses into one line: observation precedes intervention. Logs before the failure, instruments before the intervention, shadow evidence before the wiring&lt;/li&gt;
&lt;li&gt;The three patterns are layers on the same record: logs are the corpus, instruments are the lens, and shadow mode is corpus-building for a mechanism not yet allowed to act&lt;/li&gt;
&lt;li&gt;The observing machinery itself can introduce failures that distort what it observes. Auditing the shared state a shadow path touches, and the discipline of never wiring readings into behavior, are the insurance against that&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/agent-observability-patterns" rel="noopener noreferrer"&gt;agent-observability-patterns&lt;/a&gt; — the 3 skills in this article (MIT)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/agent-attribution-practice" rel="noopener noreferrer"&gt;Agent Attribution Practice&lt;/a&gt; — the corresponding ADRs (Causal Traceability / Human Approval Gate)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/contemplative-agent" rel="noopener noreferrer"&gt;Contemplative Agent&lt;/a&gt; — the project where the original patterns are in operation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shimo4228/i-built-a-skill-for-easy-codex-reviews-from-claude-code-4h89"&gt;I Built a Skill for Easy Codex Reviews from Claude Code&lt;/a&gt; — the cross-model review setup&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt; — the author's other repositories&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The three skills are generalized forks of skills from &lt;a href="https://github.com/shimo4228/contemplative-agent" rel="noopener noreferrer"&gt;Contemplative Agent&lt;/a&gt;, an autonomous agent project I operate. On the research line, they are positioned as the installable "how" that supplies the evidence demanded by &lt;a href="https://github.com/shimo4228/agent-attribution-practice" rel="noopener noreferrer"&gt;Agent Attribution Practice&lt;/a&gt; — ADR-0006 Causal Traceability ("events that change behavior must be reconstructable after the fact") and ADR-0005 Human Approval Gate ("behavior changes must be approved by a named human").&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>agents</category>
      <category>claudecode</category>
      <category>skills</category>
    </item>
    <item>
      <title>Claude Code from iPhone: Plugging 3 Holes in Remote Control</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/shimo4228/claude-code-from-iphone-plugging-3-holes-in-remote-control-17cf</link>
      <guid>https://dev.to/shimo4228/claude-code-from-iphone-plugging-3-holes-in-remote-control-17cf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: the three holes you are likely to hit when you make the official Claude iPhone app (Remote Control) your main way of driving Claude Code from outside — and how to plug each one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A while back, I wrote about &lt;a href="https://dev.to/shimo4228/running-claude-code-from-iphone-via-ssh-tmux-4c10"&gt;building an iPhone setup for Claude Code with Termius + Tailscale + tmux&lt;/a&gt;. That approach connects straight to a black SSH screen.&lt;/p&gt;

&lt;p&gt;Since then, Claude's official mobile app gained &lt;strong&gt;Remote Control&lt;/strong&gt; — a feature that lets you operate the Claude Code instance running on your Mac, remotely, as-is (available since Claude Code v2.1.51, added a few months ago). The UI is well done, and it is now my main setup. Approval prompts and progress are dramatically easier to read than on an SSH terminal.&lt;/p&gt;

&lt;p&gt;But once the official app becomes your main interface, you run into three holes. And none of them can be plugged from an iPhone alone while you are out.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hole&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;#1&lt;/strong&gt; Can't start a new session&lt;/td&gt;
&lt;td&gt;The app only connects to existing sessions&lt;/td&gt;
&lt;td&gt;Spawn one with a tmux one-liner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;#2&lt;/strong&gt; Claude auth expires&lt;/td&gt;
&lt;td&gt;The login browser opens on the Mac's side&lt;/td&gt;
&lt;td&gt;Hop into the screen via VNC just for the auth moment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;#3&lt;/strong&gt; git push fails&lt;/td&gt;
&lt;td&gt;Dies with &lt;code&gt;Device not configured&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GitHub's device flow + &lt;code&gt;--insecure-storage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;This article assumes the following are already in place. For the environment setup itself, see the earlier Termius article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote Control is enabled in the official Claude app&lt;/strong&gt;, and you can already operate Claude Code on your Mac remotely. Remote Control connects through Claude's service, so the iPhone and the Mac do not need to be on the same network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your Mac at home stays on&lt;/strong&gt; (plugged in, lid open, sleep disabled)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmux&lt;/code&gt; is installed on the Mac (if not: &lt;code&gt;brew install tmux&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;You push to GitHub via &lt;strong&gt;HTTPS + &lt;code&gt;gh auth login&lt;/code&gt;&lt;/strong&gt; (if you push with SSH keys, hole #3 does not apply to you)&lt;/li&gt;
&lt;li&gt;For the VNC re-authentication in the second half, you need a separate &lt;strong&gt;route from the iPhone to the Mac's Screen Sharing&lt;/strong&gt;. This is a different channel from Remote Control — set it up over your home LAN or a VPN like Tailscale (see the &lt;a href="https://dev.to/shimo4228/running-claude-code-from-iphone-via-ssh-tmux-4c10"&gt;previous article&lt;/a&gt; for setup)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hole #1: You can't start a new session from mobile
&lt;/h2&gt;

&lt;p&gt;The official Remote Control has one clear limitation: &lt;strong&gt;the mobile app itself cannot start a new session.&lt;/strong&gt; All it can do is list sessions already running on the Mac and connect to them. Any session you want in that list has to be started somewhere, in advance.&lt;/p&gt;

&lt;p&gt;This bites in a quiet way. You are driving project A from your iPhone and think "I want a parallel session for project B" — but you cannot trigger that launch from mobile. Your only option is to walk back to the Mac and type &lt;code&gt;claude&lt;/code&gt;, which means you are stuck whenever you are out.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Having multiple sessions on one Mac is itself fine. Launch several named &lt;code&gt;claude --remote-control&lt;/code&gt; instances and they all show up in the list. The problem is that the &lt;em&gt;launching&lt;/em&gt; cannot be triggered from the iPhone.&lt;br&gt;
&lt;/p&gt;Why not the first-party alternatives (server mode / Dispatch)?
&lt;/blockquote&gt;

&lt;p&gt;Anthropic ships two first-party alternatives (&lt;a href="https://code.claude.com/docs/en/remote-control" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;server mode (&lt;code&gt;claude remote-control&lt;/code&gt;)&lt;/strong&gt; keeps one "receptionist" process running, and it spawns new sessions on demand from the mobile side (up to &lt;code&gt;--capacity N&lt;/code&gt;). But the spawned sessions are &lt;strong&gt;tied to the directory where the receptionist was started (or worktrees of the same repository)&lt;/strong&gt;, so it does not fit the use case of spinning up sessions across multiple separate repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dispatch&lt;/strong&gt; (the Desktop integration), in my hands-on testing (as of July 2026), still felt experimental:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The execution environment appears to follow a different substrate (Cowork — Claude's agent work environment) rather than Claude Code itself, so custom skills built for Claude Code are unavailable&lt;/li&gt;
&lt;li&gt;Session startup auto-injects boilerplate instructions like "check git status, recent commits, and the layout, then report" — so every conversation opens with a long status report nobody asked for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, the spawn approach in this article is my main method.&lt;/p&gt;



&lt;h3&gt;
  
  
  Have a live session launch another session
&lt;/h3&gt;

&lt;p&gt;The idea behind the workaround is simple: &lt;strong&gt;have the session that is currently alive (the one you can operate from the iPhone) run a Bash command, and let that session launch another Claude Code on the Mac.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Claude Code starts with &lt;code&gt;--remote-control&lt;/code&gt;, it registers itself with Claude's service as a Remote Control session. Registered live sessions appear in the mobile app's list. So if you launch a new &lt;code&gt;claude --remote-control&lt;/code&gt; inside tmux, detached (in the background), the list simply gains one more entry. That is the whole trick.&lt;/p&gt;

&lt;p&gt;Here is the flow as a diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iPhone (official app / Remote Control)
   │  "Start a new session"
   ▼
Currently live session (on the Mac, RC-connected)
   │  runs the command below via Bash
   ▼
tmux (detached, holds the pty → survives disconnects)
   │  claude --remote-control "AKC"
   ▼
New Claude process → registers itself as RC → "AKC" appears in the app's list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Remote Control on the iPhone, I ask the Claude I am currently driving: "Start a new Remote Control session in &lt;code&gt;~/MyAI_Lab/agent-knowledge-cycle&lt;/code&gt;." Claude then runs this command on the Mac:&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;# 新しい Remote Control セッションを tmux で detached 起動する&lt;/span&gt;
tmux new-session &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; cc-AKC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"exec &lt;/span&gt;&lt;span class="nv"&gt;$SHELL&lt;/span&gt;&lt;span class="s2"&gt; -lc 'cd ~/MyAI_Lab/agent-knowledge-cycle &amp;amp;&amp;amp; exec claude --remote-control &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;AKC&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three points matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-d&lt;/code&gt; (detached)&lt;/strong&gt; — launches in the background. It does not hijack the calling session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login shell &lt;code&gt;-lc&lt;/code&gt;&lt;/strong&gt; — loads the PATH for &lt;code&gt;node&lt;/code&gt; / &lt;code&gt;claude&lt;/code&gt; before launching. Without this, it dies instantly with "claude not found"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tmux holds the pty (pseudo-terminal)&lt;/strong&gt; — so even if SSH or the network drops, or the calling session closes, the launched Claude keeps running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few seconds later, open the app on the iPhone and "AKC" has appeared in the session list. &lt;strong&gt;I never touched the Mac.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the actual session list. Both &lt;code&gt;zenn-content&lt;/code&gt; (where this article is being written) and &lt;code&gt;AKC&lt;/code&gt; below it were spawned this way. (The previous Termius article also had a "new session" section, but that was a different move: typing &lt;code&gt;/exit&lt;/code&gt; → &lt;code&gt;claude&lt;/code&gt; inside the single tmux session &lt;code&gt;cc&lt;/code&gt; to &lt;em&gt;swap out&lt;/em&gt; its contents. You could only hold one at a time — no parallel projects. This spawn approach lines up multiple named sessions side by side.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8zm5ne2esktyuuo829vh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8zm5ne2esktyuuo829vh.png" alt="Session list in the official app, showing zenn-content and AKC, both launched via spawn" width="800" height="1739"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sessions spawned from the phone show up in the list. Filters for "awaiting input" and "awaiting review" work too&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Typing this by hand every time is unrealistic, so I made it a launch script
&lt;/h3&gt;

&lt;p&gt;Typing that one-liner by hand every time — on an iPhone keyboard, no less — is not realistic. So I wrapped it in a launch script, &lt;code&gt;spawn.sh&lt;/code&gt;, that takes just a project name, and made it callable from a Claude Code &lt;strong&gt;skill&lt;/strong&gt; (Agent Skills — Claude Code's mechanism for scripting a specific task and invoking it as a &lt;code&gt;/command&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;From the iPhone, all I say is "Spin up an AKC session." The skill resolves "AKC" → &lt;code&gt;agent-knowledge-cycle&lt;/code&gt; and passes it to the script below. The skill is published as &lt;a href="https://github.com/shimo4228/claude-harness/blob/main/skills/spawn-session/SKILL.md" rel="noopener noreferrer"&gt;spawn-session&lt;/a&gt; (you can borrow the skill-definition style along with it).&lt;/p&gt;

spawn.sh in full (copy-paste ready)



```bash
#!/usr/bin/env bash
# spawn.sh — 新しい Claude Code (Remote Control) セッションを tmux で detached 起動する。
# Usage: spawn.sh  [display-name]
# 動作条件: tmux 3.2+（new-session の -e オプションを使うため）
set -euo pipefail

PROJECT="${1:?usage: spawn.sh  [display-name]}"
PROJECT="${PROJECT/#\~/$HOME}"                       # 先頭 ~ を展開
[[ -d "$PROJECT" ]] || { printf 'spawn.sh: no such directory: %s\n' "$PROJECT" &amp;gt;&amp;amp;2; exit 1; }

NAME="${2:-$(basename "$PROJECT")}"                  # 省略時はディレクトリ名
SESSION="cc-${NAME// /-}-$(date +%H%M%S)"            # tmux セッション名（空白は -）

# tmux を探す。PATH に無くても Homebrew の既定パス（Apple Silicon / Intel 両方）を拾う
TMUX_BIN=""
for candidate in "$(command -v tmux 2&amp;gt;/dev/null || true)" /opt/homebrew/bin/tmux /usr/local/bin/tmux; do
  [[ -n "$candidate" &amp;amp;&amp;amp; -x "$candidate" ]] &amp;amp;&amp;amp; { TMUX_BIN="$candidate"; break; }
done
[[ -n "$TMUX_BIN" ]] || { printf 'spawn.sh: tmux not found (install: brew install tmux)\n' &amp;gt;&amp;amp;2; exit 1; }

LOGIN_SHELL="${SHELL:-/bin/zsh}"

# tmux -e で値を環境変数として渡す（クォート地獄を回避）。
"$TMUX_BIN" new-session -d -s "$SESSION" -e "CCDIR=$PROJECT" -e "CCNAME=$NAME" \
  "exec $LOGIN_SHELL -lc 'cd \"\$CCDIR\" &amp;amp;&amp;amp; exec claude --remote-control \"\$CCNAME\"'"

printf '✅ Remote Control session started: "%s"\n' "$NAME"
printf '   tmux: %s\n' "$SESSION"
printf '   dir:  %s\n' "$PROJECT"

# 起動直後に落ちていないかの簡易チェック（あくまで早期検知。成功保証ではない）
sleep 1
if "$TMUX_BIN" has-session -t "$SESSION" 2&amp;gt;/dev/null; then
  printf '   (tmux session live ✓ — アプリ一覧に出たか最終確認してください)\n'
else
  printf '   ⚠️  起動直後に tmux セッションが消えました（auth 切れ / claude が PATH に無い等）\n' &amp;gt;&amp;amp;2
  exit 1
fi
```



(Comments are in Japanese; the logic reads top to bottom: expand `~`, validate the directory, derive the session name, locate tmux even off-PATH via Homebrew's default paths, pass values as environment variables with `tmux -e` to avoid quoting hell, then a 1-second early liveness check.)

The alias table ("AKC" → actual directory) is deliberately *not* baked into the script; the calling skill owns it. The script stays a pure launcher that receives an already-resolved directory and name.



&lt;p&gt;Running it returns this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Remote Control session started: "AKC"
   tmux: cc-AKC-143512
   dir:  /Users/you/MyAI_Lab/agent-knowledge-cycle
   (tmux session live ✓ — アプリ一覧に出たか最終確認してください)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The last line says: tmux session live ✓ — do a final check that it appeared in the app's list.)&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;✓&lt;/code&gt; is only an early check meaning "the tmux pane is still alive one second later." &lt;strong&gt;The real confirmation of success is the app's session list.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When ✓ and the actual outcome disagree (troubleshooting)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;✓&lt;/code&gt; appeared but the session is not in the list&lt;/strong&gt; — if &lt;code&gt;claude&lt;/code&gt; is stuck on an auth prompt or a trust confirmation, the pane stays alive (&lt;code&gt;✓&lt;/code&gt;) but the session never registers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It died without a &lt;code&gt;✓&lt;/code&gt;&lt;/strong&gt; — expired auth is the most likely cause, but a wrong PATH or a wrong flag dies the same way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In either case, the reliable move is to peek into the tmux pane first (&lt;code&gt;tmux attach -t cc-AKC-143512&lt;/code&gt;) and see what actually happened.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This workaround depends on "at least one session is alive." Only right after a Mac reboot, when nothing is running, do you need to start the first one by hand at the Mac. Then again, if the Mac is off you can't do anything from the iPhone anyway, so in practice this is a non-issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hole #2: When Claude's auth expires, you can't re-log-in from where you are
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run this setup from mobile for a few days and Claude Code's OAuth occasionally expires. Spawn in that state and it dies right at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ⚠️  起動直後に tmux セッションが消えました（auth 切れ / claude が PATH に無い等）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Translation: the tmux session vanished right after launch — expired auth, or claude missing from PATH, etc.)&lt;/p&gt;

&lt;p&gt;If it was an auth expiry, the real problem starts here. To re-authenticate, the login URL that Claude Code prints has to be opened in a browser. &lt;strong&gt;That browser opens on the home Mac — the machine being remote-controlled.&lt;/strong&gt; Nothing shows up on the iPhone in your hand. &lt;code&gt;/login&lt;/code&gt; is an operation that completes in the CLI on the Mac, and Remote Control does not proxy that browser login to your device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hop into the Mac's screen over VNC, just for the auth
&lt;/h3&gt;

&lt;p&gt;So, &lt;strong&gt;only when authenticating&lt;/strong&gt;, I go look at the Mac's actual screen. I use a VNC viewer with a free tier (RealVNC Viewer).&lt;/p&gt;

&lt;p&gt;The whole procedure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect to the home Mac with RealVNC Viewer on the iPhone&lt;/li&gt;
&lt;li&gt;Complete the Claude Code browser auth that is open on the Mac, with your finger, on the iPhone&lt;/li&gt;
&lt;li&gt;Once auth passes, close VNC and return to Remote Control in the official app&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;VNC streams the entire screen as video, so using it as a daily driver on a small iPhone screen is painful. But for &lt;strong&gt;the single moment of "tap the login button and let auth through,"&lt;/strong&gt; it is plenty. Once authenticated, you go back to the far more usable official app.&lt;/p&gt;

&lt;p&gt;The conditions for "free to use" (RealVNC licensing)&lt;/p&gt;

&lt;p&gt;Whether it is actually free depends on the VNC server on the Mac side. If you install RealVNC Server's Lite plan (free, non-commercial) on the Mac and connect to that, the whole path stays free. If instead you connect to macOS's built-in Screen Sharing, RealVNC's current licensing treats it as a "third-party VNC server," which requires a paid plan on the Viewer side. If you want to stay free, matching RealVNC on the server side is the safe bet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Security note: treat VNC with care.&lt;/strong&gt; Exposing remote access to your Mac's screen directly on a shared network like café Wi-Fi is dangerous. &lt;strong&gt;Restrict the Mac's Screen Sharing (VNC) service so it is reachable only from your home LAN or over a VPN such as Tailscale.&lt;/strong&gt; The official app's Remote Control connects via Claude's service — a separate path — so keep it mentally separate from the VNC route ("Remote Control works, therefore VNC is safe" does not follow). And always enable a strong password and encryption on VNC.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Keep auth from expiring in the first place
&lt;/h3&gt;

&lt;p&gt;VNC is the recovery tool for when auth has already expired. Better to not let it expire. One thing works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log in once while you are physically at the Mac, and keep that session alive in tmux.&lt;/strong&gt; Same idea as the tmux approach in the previous article. If sessions are launched from a locally logged-in state, the number of times you touch the auth flow at all goes down. Spawn is built on top of this "live session" too — the longer the root session lives, the less often re-authentication comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole #3: git push stops working
&lt;/h2&gt;

&lt;p&gt;After a few days of operating remotely, &lt;code&gt;git push&lt;/code&gt; stopped going through. The work is done, and the final push dies like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal: could not read Username for 'https://github.com': Device not configured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gh auth status&lt;/code&gt; says "The token in default is invalid." Looks like the token expired. Here is the trap: &lt;strong&gt;often the token is alive — it just can't be read.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The cause: SSH/tmux sessions cannot read the Keychain
&lt;/h3&gt;

&lt;p&gt;gh (the GitHub CLI) stores its token in the macOS Keychain. The login Keychain gets unlocked at &lt;strong&gt;GUI login&lt;/strong&gt;. A session over SSH, or a tmux session started by spawn, can neither unlock a locked Keychain nor display the access-permission dialog. The read fails with a "no interaction allowed" error (exit 36), gh cannot read the token and misreports it as "invalid," and git push falls over with no credentials.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/shimo4228/running-claude-code-from-iphone-via-ssh-tmux-4c10"&gt;previous article&lt;/a&gt;'s issue — Claude Code's OAuth expiring on every SSH connection — had the same root cause. In remote operation, &lt;strong&gt;every tool that depends on the Keychain hits this wall.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Diagnosing it is copy-paste:&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;# Keychain の gh トークンが読めるか（ロック中は exit 36 で失敗する）&lt;/span&gt;
security find-generic-password &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"gh:github.com"&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# SSH 経由のセッションかどうか&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SSH_TTY=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_TTY&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get &lt;code&gt;exit=36&lt;/code&gt; and &lt;code&gt;SSH_TTY&lt;/code&gt; is set, it is a Keychain access problem. Conversely, if &lt;code&gt;exit=0&lt;/code&gt; (the token &lt;em&gt;is&lt;/em&gt; readable) and gh still says invalid, the token really has expired — and the fix is the same as the next section either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: have Claude itself run the device flow
&lt;/h3&gt;

&lt;p&gt;Unlike Claude Code's OAuth (hole #2), GitHub has a &lt;strong&gt;device flow&lt;/strong&gt; — you type a one-time code into a browser on any other device to complete auth. That means &lt;strong&gt;no VNC required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I ask the Claude in the session: "Re-authenticate gh using the device flow and tell me the one-time code." What Claude runs boils down to this:&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;# バックグラウンドで認証フローを起動し、ワンタイムコードをログから読む&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | gh auth login &lt;span class="nt"&gt;-h&lt;/span&gt; github.com &lt;span class="nt"&gt;--git-protocol&lt;/span&gt; https &lt;span class="nt"&gt;--web&lt;/span&gt; &lt;span class="nt"&gt;--insecure-storage&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/gh-login.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nb"&gt;sleep &lt;/span&gt;3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/gh-login.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;! First copy your one-time code: XXXX-XXXX
Open this URL to continue in your web browser: https://github.com/login/device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, &lt;strong&gt;in the browser on the iPhone in your hand&lt;/strong&gt;, open &lt;code&gt;github.com/login/device&lt;/code&gt;, enter the code, and approve. Once approved, the auth process on the Mac side completes, and &lt;code&gt;git push&lt;/code&gt; goes straight through.&lt;/p&gt;

&lt;p&gt;I never entered the Mac's screen. Hole #2's Claude OAuth needed VNC because "where the browser opens" is pinned to the Mac; the device flow lets &lt;strong&gt;the browser open on any device, as long as the code matches&lt;/strong&gt;. That difference is what makes this work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;--insecure-storage&lt;/code&gt; actually OK?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;--insecure-storage&lt;/code&gt; flag in the command above stores the new token not in the Keychain but in &lt;code&gt;~/.config/gh/hosts.yml&lt;/code&gt; (a plaintext file with 0600 permissions). This is the permanent fix: &lt;strong&gt;the Keychain is out of the loop, so from then on &lt;code&gt;git push&lt;/code&gt; works from SSH and tmux alike.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's be precise about what "insecure" means here. Exactly one thing gets weaker: "any process running with your user privileges can read it."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Keychain storage&lt;/th&gt;
&lt;th&gt;hosts.yml plaintext&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Theft / disk read by another user&lt;/td&gt;
&lt;td&gt;Protected&lt;/td&gt;
&lt;td&gt;Effectively protected by 0600 + FileVault (when powered off)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malicious process running as you&lt;/td&gt;
&lt;td&gt;Some barrier via per-app ACLs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Readable (this is the "insecure" part)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leaking into backups / dotfiles sync&lt;/td&gt;
&lt;td&gt;The Keychain file itself is encrypted&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Can ride along in plaintext with &lt;code&gt;~/.config&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usable from SSH/tmux sessions&lt;/td&gt;
&lt;td&gt;No (our original problem)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the protection drops in two scenarios: "after you have already allowed arbitrary code execution on the machine," and "if &lt;code&gt;~/.config&lt;/code&gt; is included in backups or dotfiles sync." Even then, it is the same level as plaintext API keys in &lt;code&gt;~/.aws/credentials&lt;/code&gt; or &lt;code&gt;.env&lt;/code&gt; — a standard storage method for CLI tools. If you suspect a leak, you can revoke immediately from GitHub's &lt;a href="https://github.com/settings/applications" rel="noopener noreferrer"&gt;Applications settings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More than hardening the storage location, &lt;strong&gt;narrowing the token's permission scope is what actually limits the damage of a leak&lt;/strong&gt;. The narrowing options are folded below.&lt;/p&gt;

&lt;p&gt;If you want to narrow the blast radius further (fine-grained PAT)&lt;/p&gt;

&lt;p&gt;One option is switching to a fine-grained PAT (an access token you can scope to specific repositories with an expiry). Note that gh recommends passing fine-grained PATs via the &lt;code&gt;GH_TOKEN&lt;/code&gt; environment variable rather than registering them with &lt;code&gt;--with-token&lt;/code&gt;, since the latter can confuse the behavior of some commands. This is the narrowing move for the "all I need is push" case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finished setup
&lt;/h2&gt;

&lt;p&gt;My mobile operation has settled into this shape:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device / tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Official Claude app (Remote Control)&lt;/td&gt;
&lt;td&gt;Main UI. Progress checks, approvals, giving instructions&lt;/td&gt;
&lt;td&gt;Via Claude's service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spawn (tmux one-liner / skill)&lt;/td&gt;
&lt;td&gt;Start new sessions without touching the Mac&lt;/td&gt;
&lt;td&gt;Bash inside a live session above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RealVNC Viewer&lt;/td&gt;
&lt;td&gt;Only when Claude's auth expires: complete the Mac-side browser login from the iPhone&lt;/td&gt;
&lt;td&gt;LAN / Tailscale (VNC only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device flow (&lt;code&gt;gh auth login --web&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Recover git push auth using nothing but the iPhone's browser&lt;/td&gt;
&lt;td&gt;Via GitHub's service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The daily loop looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Operate as usual from the official app on the iPhone&lt;/li&gt;
&lt;li&gt;Need another project? "Spin up a session for X" → spawn, then confirm it appeared in the app's list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⚠️ session vanished&lt;/code&gt; = expired auth is the prime suspect → pass auth via VNC, return to the official app&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git push&lt;/code&gt; dies with &lt;code&gt;Device not configured&lt;/code&gt; → have Claude run the device flow, approve in the iPhone's browser (make it permanent with &lt;code&gt;--insecure-storage&lt;/code&gt; and it stops happening)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To be honest, the iPhone is still, as before, a "remote control." Serious reviews happen back at the Mac. But &lt;strong&gt;starting sessions without touching the Mac, recovering from auth expiry from outside, and completing the push&lt;/strong&gt; — with those three holes plugged, the situations where I am stuck while out have all but disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The official app's Remote Control has a good UI and suits mobile-first operation. But it has three holes: "you can't start a new session from mobile," "you can't fix Claude's expired auth from where you are," and "git push auth fails"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The new-session problem&lt;/strong&gt;: have a live session run &lt;code&gt;tmux new-session -d ... claude --remote-control&lt;/code&gt;. The new process registers its own Remote Control session and appears in the app's list (&lt;code&gt;✓&lt;/code&gt; is an early check; the app's list is the real confirmation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude auth expiry&lt;/strong&gt;: enter the Mac's screen via VNC only for the auth moment, and complete the browser login from the iPhone. Keep the VNC (Screen Sharing) route restricted to LAN/VPN, and treat it as separate from Remote Control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;git push auth&lt;/strong&gt;: the cause is the Keychain (unreadable from SSH/tmux). Have Claude run gh's device flow, approve in the iPhone's browser, and use &lt;code&gt;--insecure-storage&lt;/code&gt; to remove the Keychain dependency for good&lt;/li&gt;
&lt;li&gt;Environment setup (Tailscale etc.) stays in the earlier Termius article; this one focused on day-to-day operation with the official app at the center&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I abandoned the editor, passed through even the black SSH screen, and now I start sessions and finish pushes from the official app in my pocket, without touching the Mac. The remaining holes each got plugged: a tmux one-liner, VNC, and the device flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;p&gt;That's it for the tooling. One level deeper — agent design itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/shimo4228/where-react-agents-are-actually-needed-in-business-33do"&gt;Where ReAct Agents Are Actually Needed in Business&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shimo4228/i-built-a-skill-for-easy-codex-reviews-from-claude-code-4h89"&gt;I Built a Skill for Easy Codex Reviews from Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The previous mobile-setup article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/shimo4228/running-claude-code-from-iphone-via-ssh-tmux-4c10"&gt;Running Claude Code from iPhone via SSH + tmux&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skill used in this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/claude-harness/blob/main/skills/spawn-session/SKILL.md" rel="noopener noreferrer"&gt;spawn-session skill (claude-harness)&lt;/a&gt; — the spawn approach from this article, packaged as a skill&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Research artifacts (with DOIs) live at &lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>tmux</category>
      <category>mobile</category>
      <category>github</category>
    </item>
    <item>
      <title>Linters Can't Measure a Non-Code Asset's Value — I Built an LLM Stocktake for It</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:00:05 +0000</pubDate>
      <link>https://dev.to/shimo4228/linters-cant-measure-a-non-code-assets-value-i-built-an-llm-stocktake-for-it-4ng7</link>
      <guid>https://dev.to/shimo4228/linters-cant-measure-a-non-code-assets-value-i-built-an-llm-stocktake-for-it-4ng7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this article covers&lt;/strong&gt;: how to take stock of the "abandoned configs, dead workflows, and runbooks no one reads" that accumulate in a repository, using a two-tier method — &lt;strong&gt;grep for structure, an LLM for value&lt;/strong&gt;. You can drop it in as a Claude Code skill, or run the same pattern by hand without the skill.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you use it as a skill&lt;/strong&gt;: git, plus write access to &lt;code&gt;~/.claude/skills/&lt;/code&gt; (a Claude Code environment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you run it by hand&lt;/strong&gt;: a shell with &lt;code&gt;grep&lt;/code&gt;, and any LLM (the thing you hand the judgment to)&lt;/li&gt;
&lt;li&gt;The target is "files that aren't code" in general (configs, CI, docs, runbooks, etc.). Detecting dead code in the code itself is out of scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Develop with an AI agent, and non-code stuff piles up
&lt;/h2&gt;

&lt;p&gt;When you develop alongside an AI agent, files that aren't code quietly accumulate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PLAN_xxx.md&lt;/code&gt;, &lt;code&gt;HANDOFF_xxx.md&lt;/code&gt;, &lt;code&gt;PROGRESS_xxx.md&lt;/code&gt; — working notes written once and never read again&lt;/li&gt;
&lt;li&gt;Linter or formatter config files that got added temporarily and never removed&lt;/li&gt;
&lt;li&gt;GitHub Actions workflows that do effectively nothing — either their &lt;code&gt;on:&lt;/code&gt; conditions are never satisfied so they never fire, or they do fire but every job is skipped by &lt;code&gt;if: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Runbooks that are still around long after the process they describe has ended&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't "broken." The YAML is valid, the Markdown is well-formed. So the linter says nothing. &lt;strong&gt;A linter only looks at "is this structurally correct"&lt;/strong&gt; — it can't measure "does this file still deserve to exist."&lt;/p&gt;

&lt;p&gt;That was exactly my problem. I started looking for "a skill that detects runbooks nobody references anymore, or workflows that stopped functioning," and what I ended up building was a skill called &lt;code&gt;repo-asset-stocktake&lt;/code&gt;. This article walks through the core of its design (the parts anyone can reuse), and the story of actually running it against a different repository and finding an "island of orphaned documents."&lt;/p&gt;

&lt;h2&gt;
  
  
  The core: structure by grep, value by LLM
&lt;/h2&gt;

&lt;p&gt;The decisive realization before building was this: &lt;strong&gt;the properties I wanted to check split into two kinds.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;What decides it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Structural&lt;/strong&gt; (determined by the shape of the bytes)&lt;/td&gt;
&lt;td&gt;Is the YAML valid? / Is the link target alive?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Code&lt;/strong&gt; (grep / parse). 100% accurate, instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Semantic&lt;/strong&gt; (requires understanding intent)&lt;/td&gt;
&lt;td&gt;Is this config still used? / Does this runbook still describe a process that actually exists?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;LLM&lt;/strong&gt; (judgment)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code&gt;.textlintrc&lt;/code&gt; that nobody runs anymore contradicts no code (structurally, it's healthy). But its value is zero. grep can't bridge that gap.&lt;/p&gt;

&lt;p&gt;At the same time, throwing an LLM at every file up front is a waste of money. So I split it: &lt;strong&gt;enumeration is code, judgment is the LLM.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enumerate all non-code assets
        │
        ▼
tier-1 · structural: measure reachability with grep
        │
        ├─ consumer still alive ──▶ Keep (as-is)
        │
        └─ consumer gone (candidate)
                   │
                   ▼
        tier-2 · semantic: LLM judges value
                   │
                   ▼
        Keep / Update / Retire / Merge
                   │
                   ▼
        Confirm one by one, retire reversibly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tier-1 (code)&lt;/strong&gt;: measure "reachability" (explained below) with grep, and narrow down to only the suspicious candidates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tier-2 (LLM)&lt;/strong&gt;: judge only the narrowed candidates by "does this still mean anything," and emit a verdict of &lt;code&gt;Keep&lt;/code&gt; / &lt;code&gt;Update&lt;/code&gt; / &lt;code&gt;Retire&lt;/code&gt; / &lt;code&gt;Merge&lt;/code&gt;. No numeric scores.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What "reachability" means
&lt;/h3&gt;

&lt;p&gt;Reachability is "whether something that consumes this asset is still alive." &lt;strong&gt;Every non-code asset is alive because something consumes it.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer class&lt;/th&gt;
&lt;th&gt;Example asset&lt;/th&gt;
&lt;th&gt;Reachability check (what grep does)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool invocation&lt;/td&gt;
&lt;td&gt;Tool configs like &lt;code&gt;.textlintrc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Search for the tool name in &lt;code&gt;package.json&lt;/code&gt; scripts, pre-commit, Makefile, CI. Zero invocation sites = candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI trigger&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/*.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parse whether the scripts/actions it references actually exist, and whether the &lt;code&gt;on:&lt;/code&gt; trigger is reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human navigation&lt;/td&gt;
&lt;td&gt;runbook / docs&lt;/td&gt;
&lt;td&gt;Search for inbound links from other docs or the README. Zero inbound links = candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, you can measure "tool invocation" reachability like this:&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;# Check whether .textlintrc is still "invoked" (not merely installed)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"textlint"&lt;/span&gt; .github/workflows/ .pre-commit-config.yaml Makefile 2&amp;gt;/dev/null
&lt;span class="c"&gt;# For package.json, look only inside scripts (a name in devDependencies is not "invocation")&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.scripts // {} | values[]'&lt;/span&gt; package.json 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep &lt;/span&gt;textlint
&lt;span class="c"&gt;# → If neither prints anything, "zero invocation sites" = a candidate to pass to tier-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This table and these commands are what I most want you to take away from this article. If you mechanically measure, per consumer class, "whether the consumption link (the wiring to whatever calls the asset) is still intact," you can narrow the candidates a lot before ever handing anything to an LLM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I built it myself (the conclusion after looking for something off the shelf)
&lt;/h3&gt;

&lt;p&gt;The result of checking "surely a tool for this already exists" — twice&lt;/p&gt;

&lt;p&gt;At first I assumed "something off the shelf must exist" and went looking. The conclusion splits by layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For structural checks, off-the-shelf options are plentiful.&lt;/strong&gt; MegaLinter and super-linter bundle yamllint, actionlint, markdownlint, and so on. YAML validity has yamllint; dead links have dedicated tools like markdown-link-check or Lychee (both callable from MegaLinter) — note that markdownlint itself does &lt;em&gt;not&lt;/em&gt; check for dead links, so you need one of those separately. There's no reason to build any of this yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For semantic "value" review, off-the-shelf options are thin.&lt;/strong&gt; The closest is the commercial service Dosu, but from what's public, drift detection between docs and code is one of its several features (Dosu itself pitches issue triage and an agent-facing knowledge base more broadly), and in any case it isn't a mechanism for judging "does this config or workflow still hold value."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the mechanism itself — "an LLM reviews an asset by value and emits Keep/Retire" — was already implemented four times inside my own Claude Code environment (stocktake skills targeting configs, rules, skills, and project docs respectively; I wrote up the design process for the skill-targeting version in a &lt;a href="https://dev.to/shimo4228/offloading-ais-weak-spots-to-shell-scripts-designing-building-and-publishing-a-skill-audit-2ll8"&gt;separate article&lt;/a&gt;). &lt;strong&gt;What was missing wasn't the mechanism — it was the target.&lt;/strong&gt; The one aimed at "a project repository's non-code assets" was the only one that didn't exist. So instead of inventing from scratch, I copied the skeleton of an existing stocktake skill and swapped only the target.&lt;/p&gt;

&lt;p&gt;Leave the structural checks to off-the-shelf linters, and &lt;strong&gt;build only the thin "value judgment" layer that's missing&lt;/strong&gt; — that was the conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing it: as a Claude Code skill
&lt;/h2&gt;

&lt;p&gt;The skill is published as a standalone repository. Drop it in &lt;code&gt;~/.claude/skills/&lt;/code&gt; and you can call it with &lt;code&gt;/repo-asset-stocktake&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;git clone https://github.com/shimo4228/repo-asset-stocktake &lt;span class="se"&gt;\&lt;/span&gt;
  ~/.claude/skills/repo-asset-stocktake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run it, you just pass the path of the repository you want to audit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/repo-asset-stocktake                  # audit the current directory
/repo-asset-stocktake ~/path/to/repo   # audit a different repository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verdicts are recorded as a ledger in &lt;code&gt;.repo-asset-stocktake.json&lt;/code&gt; inside the audited repo, so on subsequent runs you can re-evaluate only the assets that changed. It looks like this:&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;"evaluated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-05T05:20:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assets"&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;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docs/plans/NEXT_STEPS_fsrs-migration.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"consumer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"human-navigation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reachability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0 inbound links / not listed in INDEX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Retire"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The feature already shipped, so this 'remaining work' note is now hollow"&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;Add this ledger file to &lt;code&gt;.gitignore&lt;/code&gt; and even a public repo won't leak its audit results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it for real turned up "an island of 33 orphaned files"
&lt;/h2&gt;

&lt;p&gt;After building it, I ran it against another of my own repositories (an iOS app). It has almost no CI workflows or tool configs; it's mostly Markdown docs. "So it won't find anything," I figured. The opposite happened.&lt;/p&gt;

&lt;p&gt;What came out wasn't individual junk files — it was a &lt;strong&gt;structure.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLAUDE.md (project entry point)
  ├──▶ RUNBOOK.md
  ├──▶ CONTRIB.md
  └╌╌╌ (no link) ╌╌╌▶ PROJECT_TIMELINE.md   ← the missing bridge
                          │
                          ▼
                     plans/INDEX.md
                          │
                          ▼
                     plans / reports (31 files)  ← the isolated island
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Snapshots of &lt;code&gt;PLAN&lt;/code&gt; / &lt;code&gt;PROGRESS&lt;/code&gt; / &lt;code&gt;HANDOFF&lt;/code&gt; / &lt;code&gt;BUG&lt;/code&gt; / &lt;code&gt;REVIEW&lt;/code&gt;, born during a two-week development burst five months earlier, had piled up in &lt;code&gt;docs/plans/&lt;/code&gt; and &lt;code&gt;docs/reports/&lt;/code&gt; — &lt;strong&gt;31 files.&lt;/strong&gt; The only path to them was a single chain: &lt;code&gt;PROJECT_TIMELINE.md&lt;/code&gt; → &lt;code&gt;INDEX.md&lt;/code&gt;. But &lt;code&gt;PROJECT_TIMELINE.md&lt;/code&gt; itself wasn't linked from &lt;code&gt;CLAUDE.md&lt;/code&gt; (the project's entry point). Add those two entry-point files, and you get &lt;strong&gt;33 files unreachable from the entry point.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The division of labor between tier-1's grep and tier-2's LLM paid off exactly here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tier-1 (grep)&lt;/strong&gt;: list the structural facts — "&lt;code&gt;PROJECT_TIMELINE.md&lt;/code&gt; has 0 inbound links," "&lt;code&gt;INDEX.md&lt;/code&gt; links to the plan set." That the bridge is isolated falls straight out of grep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tier-2 (LLM)&lt;/strong&gt;: interpret those facts as "the only bridge to the whole island is severed — reconnect one entry point and 33 files come back to life," and choose reconnection over deletion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detection is structural (tier-1); the meaning is the LLM (tier-2). The reason I split enumeration from judgment worked exactly as intended on this island.&lt;/p&gt;

&lt;p&gt;The fix wasn't deletion — it was &lt;strong&gt;reconnecting one entry point.&lt;/strong&gt; Adding a single line in &lt;code&gt;CLAUDE.md&lt;/code&gt; linking to &lt;code&gt;PROJECT_TIMELINE.md&lt;/code&gt; rescues the history of all 33 files at once. That's a conclusion a lint could never produce, and one that only a value review could.&lt;/p&gt;

&lt;h3&gt;
  
  
  The moment tier-2 delivered more value than "delete"
&lt;/h3&gt;

&lt;p&gt;The most effective thing this skill did wasn't deletion — it was &lt;strong&gt;vetoing a deletion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;tier-1 found two files both named &lt;code&gt;dead-code-analysis.md&lt;/code&gt; and flagged them as a "duplicate" candidate. With naive basename dedup, this is where one of them would have been deleted.&lt;/p&gt;

&lt;p&gt;But when tier-2 compared their contents, one was an analysis by Python's vulture and the other was a manual Swift analysis — &lt;strong&gt;different things.&lt;/strong&gt; Merging them would lose one of the analyses. The verdict was "cancel the Merge." The semantic layer verifies and overturns the structural signal. That's exactly the point of splitting it into two tiers.&lt;/p&gt;

&lt;p&gt;In the same vein, over on the &lt;code&gt;zenn-content&lt;/code&gt; side it caught an asset of a different class: &lt;code&gt;.gitignore&lt;/code&gt; declared that &lt;code&gt;archive/&lt;/code&gt; should "not be published," yet four files committed before that rule was added were still tracked and continued to be published on GitHub — a gap between the intent of a config and reality. The same skill spans assets of different consumer classes: tool configs, workflows, docs, and VCS settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can run this pattern without the skill
&lt;/h2&gt;

&lt;p&gt;Even without a Claude Code custom skill, the core is reusable. With your own agent (or by hand), just do the following in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enumerate (code)&lt;/strong&gt;: list every non-code asset, and measure reachability per consumer class with grep (the table and commands under "What 'reachability' means" above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow&lt;/strong&gt;: take the ones with zero reachability (0 invocation sites / all references dead / 0 inbound links), &lt;em&gt;plus&lt;/em&gt; the ones that "are reachable but look suspicious" (stale updates, throwaway docs like &lt;code&gt;PLAN&lt;/code&gt; / &lt;code&gt;HANDOFF&lt;/code&gt;, notes describing "remaining work" for an already-shipped feature, etc.) — the island in the previous section still had inbound links, yet it was hollow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge (LLM)&lt;/strong&gt;: judge only the candidates by "does this still mean anything," and emit Keep / Update / Retire / Merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete reversibly&lt;/strong&gt;: don't delete a Retire immediately — first move it somewhere recoverable, like renaming to &lt;code&gt;.disabled&lt;/code&gt; (soft-delete), and confirm one by one&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The biggest pitfall is that &lt;strong&gt;"zero reachability" does not equal "dead."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I built this skill, I ran a review by a different model (Codex) in parallel, and it caught a blind spot my own review (Claude) had missed: &lt;strong&gt;when consumption is one step indirect, grep comes up empty.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prettier invoked via &lt;code&gt;lint-staged&lt;/code&gt; has no direct invocation site&lt;/li&gt;
&lt;li&gt;a remote &lt;code&gt;uses: owner/repo@ref&lt;/code&gt; action is alive even with no local copy present&lt;/li&gt;
&lt;li&gt;a doc listed in the nav of &lt;code&gt;mkdocs.yml&lt;/code&gt; is reachable even with no Markdown inbound links&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/skills/*.md&lt;/code&gt; has 0 inbound links, but its consumer isn't a human — it's the Claude Code loader&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat zero reachability not as a "deletion candidate" but as &lt;strong&gt;"needs investigation."&lt;/strong&gt; Mistake the consumer, and you'll kill a live asset. In fact, without this correction I'd have wrongly Retired 9 of the iOS repo's &lt;code&gt;.claude/skills/*.md&lt;/code&gt; files.&lt;/p&gt;
&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Leave the &lt;strong&gt;structure&lt;/strong&gt; of non-code assets (validity, dead links) to off-the-shelf linters. Don't build it yourself.&lt;/li&gt;
&lt;li&gt;Layer a thin LLM only over &lt;strong&gt;value&lt;/strong&gt; (does this still deserve to exist). Don't run it on everything — narrow the candidates with grep first.&lt;/li&gt;
&lt;li&gt;Measuring reachability per consumer class (tool invocation / CI trigger / human navigation) is the mechanical axis for that narrowing.&lt;/li&gt;
&lt;li&gt;Zero reachability is "needs investigation," not "delete." Miss an indirect consumer and you'll kill a live asset.&lt;/li&gt;
&lt;li&gt;Always delete reversibly (move to &lt;code&gt;.disabled&lt;/code&gt; → confirm one by one).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lint can only answer "is this file correct." "Is this file still needed" is a different question — one that requires understanding meaning. Hand only that part to an LLM, and assets that had slipped through the linter's net start to become visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The skill itself: &lt;a href="https://github.com/shimo4228/repo-asset-stocktake" rel="noopener noreferrer"&gt;shimo4228/repo-asset-stocktake&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Design process of a sister skill: &lt;a href="https://dev.to/shimo4228/offloading-ais-weak-spots-to-shell-scripts-designing-building-and-publishing-a-skill-audit-2ll8"&gt;Offloading AI's Weak Spots to Shell Scripts — Designing, Building, and Publishing a Skill Audit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The author's skills and research repositories: &lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;github.com/shimo4228&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>devtools</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>I Built a Skill for Easy Codex Reviews from Claude Code</title>
      <dc:creator>Tatsuya Shimomoto</dc:creator>
      <pubDate>Sun, 05 Jul 2026 04:30:06 +0000</pubDate>
      <link>https://dev.to/shimo4228/i-built-a-skill-for-easy-codex-reviews-from-claude-code-4h89</link>
      <guid>https://dev.to/shimo4228/i-built-a-skill-for-easy-codex-reviews-from-claude-code-4h89</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;What this article covers: how to add exactly one cross-model review step to Claude Code using the OpenAI Codex CLI, and why it works — not because of "more" review, but because it catches "a different class of blind spot."&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;Codex CLI&lt;/a&gt; installed and authenticated (verify with &lt;code&gt;codex login&lt;/code&gt; or &lt;code&gt;codex doctor&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Working inside a &lt;code&gt;git&lt;/code&gt; repository from Claude Code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Usage — a read-only second opinion in one command
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;codex-review&lt;/code&gt; is a read-only skill that thinly wraps &lt;code&gt;codex review&lt;/code&gt; (the OpenAI Codex CLI). It shows the current diff to a model from a different lineage than Claude Code and has it review the work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/codex-review&lt;/code&gt; isn't a command that ships with Claude Code out of the box — it's a small wrapper script I placed in my own Claude Code harness.&lt;/p&gt;

&lt;p&gt;The skill itself is published as &lt;a href="https://github.com/shimo4228/codex-review" rel="noopener noreferrer"&gt;shimo4228/codex-review&lt;/a&gt;, so you can reproduce the same setup by copying &lt;code&gt;skills/codex-review/&lt;/code&gt; from that repository into &lt;code&gt;~/.claude/skills/codex-review/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All it does is a single shell script that passes only the allow-listed flags straight through to &lt;code&gt;codex review&lt;/code&gt; and rejects everything else.&lt;/p&gt;

&lt;p&gt;The script itself isn't Claude Code-specific. If you look inside, the string "Claude" only appears in comments — the actual execution logic is just &lt;code&gt;bash&lt;/code&gt; + &lt;code&gt;git&lt;/code&gt; + the &lt;code&gt;codex&lt;/code&gt; CLI.&lt;/p&gt;

&lt;p&gt;You can run it straight from a terminal, call it from a different agent CLI, or use it as a plain pre-commit hook with no Claude Code in the loop, and it works the same way. Being "a Claude Code skill" is just a matter of where you put it and how you invoke it.&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;# current branch vs. auto-detected base branch (PR style)&lt;/span&gt;
/codex-review

&lt;span class="c"&gt;# staged + unstaged + untracked files (for a pre-commit check)&lt;/span&gt;
/codex-review &lt;span class="nt"&gt;--uncommitted&lt;/span&gt;

&lt;span class="c"&gt;# against a specific branch or commit&lt;/span&gt;
/codex-review &lt;span class="nt"&gt;--base&lt;/span&gt; main
/codex-review &lt;span class="nt"&gt;--commit&lt;/span&gt; &amp;lt;sha&amp;gt;

&lt;span class="c"&gt;# prompt-driven (review the whole working tree with no scope flag)&lt;/span&gt;
/codex-review &lt;span class="s2"&gt;"just look at the auth-related changes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read-only is enforced at the code level. Internally it only ever calls &lt;code&gt;codex review&lt;/code&gt; (which never rewrites code), and any flag not on the allow list is rejected outright. Even if the Codex CLI adds a write-capable flag in the future, it won't run unless it's added to the allow list.&lt;/p&gt;

&lt;p&gt;I don't paste Codex's output straight into the parent conversation — I verify it first, then summarize.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent: codex-review
Verdict: &amp;lt;CRITICAL | HIGH | MEDIUM | LOW | CLEAN&amp;gt;
Findings (top 3): &amp;lt;only what's been confirmed&amp;gt;
Files touched: &amp;lt;path:line&amp;gt;
Next action: &amp;lt;continue | stop | re-plan&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I treat Codex's findings as "external input that isn't necessarily correct," and keep only the ones I can confirm by reading the code and reproducing the issue. Deciding the verdict is still on me (the Claude side).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into the harness as an "automatically-run check"
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;/codex-review&lt;/code&gt; manually every time is already useful, but it really pays off once it becomes something you can't forget to call. Write the condition into your project rules (&lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;.claude/rules/*.md&lt;/code&gt;) and, once implementation reaches a natural checkpoint, Claude will follow it on its own — without you asking.&lt;/p&gt;

&lt;p&gt;That said, this isn't deterministic enforcement like a hook. A rule only conditions Claude's judgment; whether it actually runs is still up to Claude in the end (if you want a 100% guarantee, you need to enforce it with a PostToolUse hook).&lt;/p&gt;

&lt;p&gt;Even so, just writing "once you've made a non-trivial diff" into the rule as an explicit condition changes the pattern from "ask each time" to "it runs on its own once the condition is met." The full rule I actually use (with its Chain Matrix and early-stop conditions) is published in &lt;a href="https://github.com/shimo4228/claude-harness/blob/main/rules/common/planning.md" rel="noopener noreferrer"&gt;claude-harness's &lt;code&gt;planning.md&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Example: write this into CLAUDE.md or .claude/rules/*.md --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## Review step (right after implementation, before commit)&lt;/span&gt;

When a feat/fix produces a non-trivial diff, run the following in parallel before committing:
&lt;span class="p"&gt;-&lt;/span&gt; a normal code review (a subagent on your own model)
&lt;span class="p"&gt;-&lt;/span&gt; codex-review (cross-model, read-only)

If either one returns CRITICAL, stop the commit and report to the user.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make the trigger "a non-trivial diff was written," not "the user asked for it."&lt;/strong&gt; This structurally prevents forgetting to call it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop early on a CRITICAL finding.&lt;/strong&gt; If a different model flags something serious, the rule itself gates on stopping the commit right there and deferring to human judgment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set up this way, findings aren't "something I happened to notice" — they get caught in the same place every time. This article itself ended up being a live demonstration of that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;codex-review&lt;/code&gt; doesn't just review code diffs — in prompt-driven mode it can review prose too. This repository's pre-publish checklist says "articles get reviewed in parallel by editor / fact-checker / codex-review," and this article went through codex-review without me explicitly asking for it either.&lt;/p&gt;

&lt;p&gt;And it actually caught something. The paragraph right before this one had originally overclaimed that "writing it into a rule makes it run automatically" — that's the finding, and the current wording already reflects the fix.&lt;/p&gt;

&lt;p&gt;Not just blind spots in code — a different model diligently catches overclaiming in prose too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually found — a different model sees a different blind spot
&lt;/h2&gt;

&lt;p&gt;It's tempting to describe the value of codex-review as "it's good at finding bugs Claude alone would miss," but framed as a frequency claim, that's hard to verify. Here's what actually happened, described plainly.&lt;/p&gt;

&lt;p&gt;This is from a review on &lt;a href="https://github.com/shimo4228/contemplative-agent" rel="noopener noreferrer"&gt;Contemplative Agent&lt;/a&gt;, another research repository of mine, when I added a single monitoring instrument module (&lt;a href="https://github.com/shimo4228/contemplative-agent/blob/main/docs/adr/0071-read-only-pattern-composition-instruments.md" rel="noopener noreferrer"&gt;ADR-0071&lt;/a&gt;, &lt;a href="https://github.com/shimo4228/contemplative-agent/commit/224fdd97b740df7bf1b2a18bc77f6e8cc4980ec0" rel="noopener noreferrer"&gt;the commit in question&lt;/a&gt;). This instrument is observation-only by design: it never touches pipeline behavior and only reads and displays the distribution and aggregates of embedding vectors. I ran &lt;code&gt;codex-review&lt;/code&gt; (a different model lineage) and Claude's own regular review (&lt;code&gt;python-reviewer&lt;/code&gt;) in parallel against the same diff.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reviewer&lt;/th&gt;
&lt;th&gt;What it found&lt;/th&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;codex-review&lt;/td&gt;
&lt;td&gt;A crash path in the aggregation logic when embedding vector dimensions are inconsistent&lt;/td&gt;
&lt;td&gt;Numeric / data-shape mismatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codex-review&lt;/td&gt;
&lt;td&gt;A filter condition in the aggregation was off by one step from the processing it was supposed to observe&lt;/td&gt;
&lt;td&gt;Semantic drift in aggregation logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codex-review&lt;/td&gt;
&lt;td&gt;The dry-run aggregation was counting candidates that should have been dropped by downstream deduplication&lt;/td&gt;
&lt;td&gt;Semantic drift in aggregation (scope)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;python-reviewer&lt;/td&gt;
&lt;td&gt;An unexpected input (e.g., a non-numeric embedding) propagates an exception that stops the host pipeline it was only supposed to be observing&lt;/td&gt;
&lt;td&gt;Exception-handling / robustness contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 3:1 split is just the result of this one run — the point isn't the count, it's the difference in the level of abstraction of the findings (more on that right after the table).&lt;/p&gt;

&lt;p&gt;I confirmed all four by reading the code and reproducing the issue, then fixed them and added regression tests (the primary record is in the &lt;a href="https://github.com/shimo4228/contemplative-agent/commit/224fdd97b740df7bf1b2a18bc77f6e8cc4980ec0" rel="noopener noreferrer"&gt;commit message&lt;/a&gt;). All tests stayed green the whole time these four bugs existed. In other words, "it runs" and "it's correct" are separate axes of checking, and review was covering the axis tests weren't.&lt;/p&gt;

&lt;p&gt;What's interesting is finding #1 (codex-review: crash on dimension mismatch) and finding #4 (python-reviewer: host pipeline halted by exception propagation). On the surface, both look like the same "broken embedding row" problem.&lt;/p&gt;

&lt;p&gt;They aren't, though — the findings sit at different levels of abstraction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;codex-review&lt;/strong&gt; — a specific failure mode: the aggregation function crashes when dimensions don't match&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;python-reviewer&lt;/strong&gt; — a broader contract that failure mode sits inside: the design principle that "an observation-only instrument must never halt the host pipeline, no matter what abnormal input arrives" was missing entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Around the same underlying phenomenon — a "broken row" — the two reviewers pointed at different levels of abstraction, different variants of the same problem. That's what happened here.&lt;/p&gt;

&lt;p&gt;I can't claim that running the same model lineage twice would only ever surface one or the other. What I can say is that, in this one run at least, there was no overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls / Tips
&lt;/h2&gt;

&lt;p&gt;If you hit one of these&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope flags and a free-form prompt are mutually exclusive.&lt;/strong&gt; Passing &lt;code&gt;--uncommitted&lt;/code&gt; (or similar) together with a free-form prompt gets rejected with exit code 64.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you run with no scope argument and HEAD is already on the base branch&lt;/strong&gt; (e.g., running with no flags while on &lt;code&gt;main&lt;/code&gt;), the diff comes out empty, so it automatically falls back to &lt;code&gt;--uncommitted&lt;/code&gt;. This auto-fallback does not kick in if you specify a scope explicitly, e.g. &lt;code&gt;--base main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the CLI isn't installed&lt;/strong&gt;, the wrapper itself checks upfront and fails early. &lt;strong&gt;If it's not authenticated&lt;/strong&gt;, that's outside the wrapper's scope — &lt;code&gt;codex review&lt;/code&gt; itself returns the error (check with &lt;code&gt;codex login&lt;/code&gt; / &lt;code&gt;codex doctor&lt;/code&gt;). Either way, just continue with Claude's own review alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;codex-review&lt;/code&gt; itself is a thin wrapper, but what makes it work is the combination of two things: the design choice to mix in exactly one other model, and the operational choice to write "a non-trivial diff makes you a review target" explicitly into a rule. Add a few lines to your own project rules and you can reproduce the same setup starting today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/codex-review" rel="noopener noreferrer"&gt;The codex-review skill itself&lt;/a&gt; — install it by copying &lt;code&gt;skills/codex-review/&lt;/code&gt; to &lt;code&gt;~/.claude/skills/codex-review/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/claude-harness/blob/main/rules/common/planning.md" rel="noopener noreferrer"&gt;claude-harness's &lt;code&gt;planning.md&lt;/code&gt;&lt;/a&gt; — the full review step, Chain Matrix, and early-stop conditions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228/contemplative-agent" rel="noopener noreferrer"&gt;Contemplative Agent&lt;/a&gt; / &lt;a href="https://github.com/shimo4228/contemplative-agent/blob/main/docs/adr/0071-read-only-pattern-composition-instruments.md" rel="noopener noreferrer"&gt;ADR-0071&lt;/a&gt; / &lt;a href="https://github.com/shimo4228/contemplative-agent/commit/224fdd97b740df7bf1b2a18bc77f6e8cc4980ec0" rel="noopener noreferrer"&gt;the commit in question&lt;/a&gt; — the real example used here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;Codex CLI (upstream)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shimo4228" rel="noopener noreferrer"&gt;shimo4228 on GitHub&lt;/a&gt; — list of other public repositories&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>codereview</category>
      <category>codex</category>
      <category>claudecode</category>
      <category>skills</category>
    </item>
  </channel>
</rss>
