<?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: Sho Naka</title>
    <description>The latest articles on DEV Community by Sho Naka (@nomurasan).</description>
    <link>https://dev.to/nomurasan</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%2F3987494%2Fa151ecf0-1dfb-44a7-91d6-5bafe84ecdb8.jpg</url>
      <title>DEV Community: Sho Naka</title>
      <link>https://dev.to/nomurasan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nomurasan"/>
    <language>en</language>
    <item>
      <title>I Used Claude Code Heavily. Then Codex Changed How I Hand Off Work.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Wed, 12 Aug 2026 09:47:31 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-used-claude-code-heavily-then-codex-changed-how-i-hand-off-work-id</link>
      <guid>https://dev.to/nomurasan/i-used-claude-code-heavily-then-codex-changed-how-i-hand-off-work-id</guid>
      <description>&lt;p&gt;I used to hand work to Codex the moment Git, a CLI, or code entered the conversation. I thought that would make execution faster.&lt;/p&gt;

&lt;p&gt;What grew was not execution speed. It was the number of handoffs: frame the requirement in ChatGPT, inspect the repository in Codex, return to ChatGPT to change an assumption, then send it back to Codex. A round trip that brings back a new fact from the real environment is valuable. I was also moving questions that could still be decided without running anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: I stopped routing work by whether it looked “technical.” While the next decision can be made safely from the current context, I let AI do the foundation work too: research, comparison, and structure. When the answer moves into the real repository, Git history, CLI, tests, or browser, I hand Codex an end-to-end execution unit using five fields.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was my routing rule, not Codex
&lt;/h2&gt;

&lt;p&gt;I have used &lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Anthropic's Claude Code&lt;/a&gt; as more than code completion. It can read a codebase, edit files, and run commands. I gave it research, design, implementation, tests, and Git work as one long unit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Claude Code Hooks&lt;/a&gt; were an important part of that workflow. A Hook is a user-defined command that runs at a particular lifecycle event in Claude Code. It can format after an edit, stop before a dangerous command, or start a verification step. In practical terms, Hooks let me turn repeated instructions into repeatable stopping points.&lt;/p&gt;

&lt;p&gt;I still keep the operating idea I learned there: once I give an AI a well-defined unit of work, I want it to finish the investigation, decisions, execution, and verification.&lt;/p&gt;

&lt;p&gt;When I started using the &lt;a href="https://openai.com/index/introducing-the-codex-app/" rel="noopener noreferrer"&gt;Codex app&lt;/a&gt; seriously, I tried to apply the same idea across ChatGPT and Codex. But I used a rough rule for where to cut the work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If it becomes technical, move it to Codex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds natural, but “technical” is not an execution requirement. Defining a reader, comparing existing approaches, choosing constraints, and writing a done condition can all include technical language. None of those decisions necessarily needs the real repository yet.&lt;/p&gt;

&lt;p&gt;I was confusing the topic of the work with the evidence the work required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a handoff actually carries
&lt;/h2&gt;

&lt;p&gt;By &lt;em&gt;handoff&lt;/em&gt;, I mean moving work from ChatGPT to Codex, or from one AI task to another. It is not just copying a prompt. It is transferring enough information for the next worker to continue the same decision.&lt;/p&gt;

&lt;p&gt;That information is &lt;em&gt;context&lt;/em&gt;. In AI systems, context is the information visible to the model when it decides its next response or action. Operationally, I treat context as a handoff packet: the goal, current state, decisions already made, fixed constraints, next action, and evidence that will prove success.&lt;/p&gt;

&lt;p&gt;In my workflow, I rebuilt that packet for each move. If the decisions are still unstable, the next AI may research the same question again or act on a different assumption.&lt;/p&gt;

&lt;p&gt;In an earlier Zenn article, &lt;a href="https://zenn.dev/nomurasan/articles/measure-context-injection-byte-budget" rel="noopener noreferrer"&gt;I wrote about delivering important context reliably after a handoff&lt;/a&gt; without making the injected packet too large. That problem still matters. But there is a cheaper question to ask first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this handoff need to happen yet?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Route by where the next answer lives
&lt;/h2&gt;

&lt;p&gt;My current routing rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the next decision can be made safely from known goals and constraints, continue in the current surface. If getting the answer requires touching the real environment, hand it to Codex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;“Continue in the current surface” does not mean I write everything myself. I am moving foundation work to AI to test how much of my own load it can remove.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Research and compare existing information&lt;/li&gt;
&lt;li&gt;Frame the reader's or user's problem&lt;/li&gt;
&lt;li&gt;Propose scope and non-scope&lt;/li&gt;
&lt;li&gt;Build an outline and first draft&lt;/li&gt;
&lt;li&gt;Propose done conditions and verification methods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human keeps a different class of decisions: whose problem matters, what must not change, where the publication, deletion, spending, or other irreversible boundaries are, and what observed result should count as success.&lt;/p&gt;

&lt;p&gt;This is not a compromise between dumping everything on AI and reviewing everything as a human. It is an operating design that gives the human and AI different kinds of ownership.&lt;/p&gt;

&lt;p&gt;When the real environment holds the next answer, I move the work to Codex.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Next question&lt;/th&gt;
&lt;th&gt;Where the answer lives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whose problem should this change solve?&lt;/td&gt;
&lt;td&gt;Current requirements and human judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does a similar implementation already exist?&lt;/td&gt;
&lt;td&gt;The real repository and Git history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does this command work in the current environment?&lt;/td&gt;
&lt;td&gt;CLI output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the change break existing behavior?&lt;/td&gt;
&lt;td&gt;Tests, lint, and build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can a reader use the actual page?&lt;/td&gt;
&lt;td&gt;The real browser surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The boundary is not between thinking and building. It is between &lt;strong&gt;deciding from available information and collecting evidence from the real artifact&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a five-field handoff
&lt;/h2&gt;

&lt;p&gt;Once the boundary is clear, I do not need a long specification. This is the smallest packet I now try to fix before Codex starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Who has what problem, and what changes for them?&lt;/span&gt;
&lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Which decisions are fixed? What must not change? Where are the publish, delete, and spend gates?&lt;/span&gt;
&lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;What is the current state? What remains to inspect or change in the real environment?&lt;/span&gt;
&lt;span class="na"&gt;done_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;What observable state means the work is complete?&lt;/span&gt;
&lt;span class="na"&gt;verify_with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Which command, screen, URL, test, or diff proves it?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves implementation freedom while fixing the semantic boundaries.&lt;/p&gt;

&lt;p&gt;For example, suppose the goal is “make a Zenn article reviewable.” Before I ask Codex to open a repository, AI can research candidate readers, check overlap with existing articles, propose the claim, build an outline, and define what the cover image needs to communicate.&lt;/p&gt;

&lt;p&gt;The human decides whether the claim matches lived experience, what is safe to publish, and what “reviewable” means. Codex can then find the canonical files, place the article and image, run Zenn Preview, and check the table of contents, code blocks, links, image, and frontmatter on the real rendered page.&lt;/p&gt;

&lt;p&gt;The five fields preserve technical choice while giving Codex enough meaning to act autonomously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for evidence, not routine decisions
&lt;/h2&gt;

&lt;p&gt;After the handoff, I do not want Codex to ask me about every small reversible choice. I want it to own this loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspect -&amp;gt; Decide -&amp;gt; Change -&amp;gt; Test -&amp;gt; Repair -&amp;gt; Verify the real artifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A return trip from Codex to the planning surface is valuable when execution produced a new fact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API we expected does not exist&lt;/li&gt;
&lt;li&gt;A test disproved a requirement assumption&lt;/li&gt;
&lt;li&gt;The real UI is technically correct but hard to read&lt;/li&gt;
&lt;li&gt;An existing implementation makes new code unnecessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those handoffs are worth their cost because they carry evidence that was unavailable at the desk.&lt;/p&gt;

&lt;p&gt;Reversible choices such as “should this variable be named A or B?” or “which of these equivalent libraries should we use?” usually are not worth a return trip. I am testing whether transferring ownership of the execution loop reduces the human load too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I cannot claim yet
&lt;/h2&gt;

&lt;p&gt;I cannot say this boundary reduced my handoff count or completion time by a particular percentage. I have not measured that yet.&lt;/p&gt;

&lt;p&gt;What I can claim today is improved explainability. When I stabilize decisions until the real environment is needed, it becomes easier to state what I am handing to Codex, why it is moving now, and what evidence will finish it.&lt;/p&gt;

&lt;p&gt;To test the method, I plan to measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handoffs per unit of work&lt;/li&gt;
&lt;li&gt;Premises rebuilt after a handoff&lt;/li&gt;
&lt;li&gt;Clarification requests returned to the human&lt;/li&gt;
&lt;li&gt;Time from execution start to a verified artifact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I may change the boundary again in a few weeks. That would not make this a failure. It would mean the operating idea became a measurable hypothesis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your next task
&lt;/h2&gt;

&lt;p&gt;On your next AI-assisted task, stop before writing “technical work.” Fill the five fields, then ask one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the next decision be made safely without the real repository, CLI, test result, or browser?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If yes, let AI continue the foundation work in the current surface.&lt;/p&gt;

&lt;p&gt;If no, move the work to the environment that has the answer and hand over execution and verification together.&lt;/p&gt;

&lt;p&gt;The important idea I kept from Claude Code was to let AI own a long unit of work. What I changed for Codex was where that autonomous unit begins.&lt;/p&gt;

&lt;p&gt;My current hypothesis is that handing off only the code leaves too much coordination with the human. So I am testing a broader split: move the foundation work to AI, keep the semantic boundaries human-owned, and move execution when the real environment has the answer.&lt;/p&gt;

&lt;p&gt;What signal currently tells you to hand work to another agent?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;This article was adapted from &lt;a href="https://zenn.dev/nomurasan/articles/claude-code-to-codex-handoff" rel="noopener noreferrer"&gt;my Japanese Zenn article&lt;/a&gt; with AI assistance for English restructuring, copy editing, and cover art. The workflow described here is my own; factual claims were checked against the source article and official product documentation.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
      <category>codex</category>
    </item>
    <item>
      <title>My AI Answered in 5.8 Seconds and Said Nothing Useful. I Almost Blamed the Model.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 08 Aug 2026 09:42:56 +0000</pubDate>
      <link>https://dev.to/nomurasan/my-ai-answered-in-58-seconds-and-said-nothing-useful-i-almost-blamed-the-model-fh1</link>
      <guid>https://dev.to/nomurasan/my-ai-answered-in-58-seconds-and-said-nothing-useful-i-almost-blamed-the-model-fh1</guid>
      <description>&lt;p&gt;I put an AI into a Google Meet call. It transcribed Japanese, generated a reply, and spoke it&lt;br&gt;
out loud. Total new spend: &lt;strong&gt;$0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then I asked it the one question I actually needed answered, and it said:&lt;br&gt;
&lt;em&gt;"I think there's still room for discussion. How about we set up a session to align our understanding?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is exactly what a person says when they don't know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: I had a latency problem and an "is this model smart enough" problem. Neither was real.&lt;br&gt;
Same model, same question, 5.80s → 5.68s — 2,545 characters of context turned a deflection into&lt;br&gt;
a claim you could argue with.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The stack, and what it replaced
&lt;/h2&gt;

&lt;p&gt;I wanted an AI participant in a real meeting. Not a note-taker — something that answers when&lt;br&gt;
someone demands specifics.&lt;/p&gt;

&lt;p&gt;The obvious stack bills you three times: a hosted meeting-bot API, a speech-to-text vendor, and&lt;br&gt;
a text-to-speech vendor. I replaced all three.&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;Obvious choice&lt;/th&gt;
&lt;th&gt;What I used&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;Meeting bot&lt;/td&gt;
&lt;td&gt;Recall.ai, $0.50/hour&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Attendee&lt;/strong&gt; (OSS, self-hosted)&lt;/td&gt;
&lt;td&gt;no per-hour billing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speech-to-text&lt;/td&gt;
&lt;td&gt;Deepgram / AssemblyAI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google Meet's own captions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the meeting already generates them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech&lt;/td&gt;
&lt;td&gt;Google Cloud TTS&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;raw audio POST&lt;/strong&gt; (below)&lt;/td&gt;
&lt;td&gt;no GCP project at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning + voice&lt;/td&gt;
&lt;td&gt;LLM + TTS, two hops&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemini Live&lt;/strong&gt; (speech-to-speech)&lt;/td&gt;
&lt;td&gt;one model, one hop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/attendee-labs/attendee" rel="noopener noreferrer"&gt;Attendee&lt;/a&gt; is 699 stars, last pushed 2026-08-07.&lt;br&gt;
Google Meet exposes no bot API, so it drives a full Chrome instance — which is why setup hurt&lt;br&gt;
before anything else did.&lt;/p&gt;
&lt;h3&gt;
  
  
  The setup tax, compressed
&lt;/h3&gt;

&lt;p&gt;Two problems were routine. The image pins &lt;code&gt;FROM --platform=linux/amd64 ubuntu:22.04&lt;/code&gt;, and my&lt;br&gt;
machine is Apple Silicon, so colima with Rosetta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;colima start &lt;span class="nt"&gt;--vm-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vz &lt;span class="nt"&gt;--vz-rosetta&lt;/span&gt; &lt;span class="nt"&gt;--cpu&lt;/span&gt; 6 &lt;span class="nt"&gt;--memory&lt;/span&gt; 12 &lt;span class="nt"&gt;--disk&lt;/span&gt; 60
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux/amd64 alpine:3.20 &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;   &lt;span class="c"&gt;# x86_64, 5.6s cold&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the build died at step 35 of 42 with &lt;code&gt;the --chmod option requires BuildKit&lt;/code&gt; — colima's&lt;br&gt;
docker CLI ships without the buildx plugin. &lt;code&gt;brew install docker-buildx&lt;/code&gt;, point&lt;br&gt;
&lt;code&gt;~/.docker/config.json&lt;/code&gt; at &lt;code&gt;/opt/homebrew/lib/docker/cli-plugins&lt;/code&gt; via &lt;code&gt;cliPluginsExtraDirs&lt;/code&gt;,&lt;br&gt;
rebuild. &lt;code&gt;docker images&lt;/code&gt; then reported three images at 5.91 GB each.&lt;/p&gt;

&lt;p&gt;The third problem is the one worth your time, because it produced &lt;strong&gt;no error at all&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;/usr/bin/python: can&lt;span class="s1"&gt;'t open file '&lt;/span&gt;/attendee/init_env.py&lt;span class="s1"&gt;': [Errno 2] No such file or directory
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file existed on the host. &lt;code&gt;dev.docker-compose.yaml&lt;/code&gt; mounted &lt;code&gt;.:/attendee&lt;/code&gt;. Both true — and&lt;br&gt;
the mount was still empty, because &lt;strong&gt;colima only mounts &lt;code&gt;$HOME&lt;/code&gt;&lt;/strong&gt;, and my checkout was under&lt;br&gt;
&lt;code&gt;/private/tmp/...&lt;/code&gt;. No warning, no failed-mount message, just an empty directory that looked&lt;br&gt;
exactly like a correctly-mounted one.&lt;/p&gt;

&lt;p&gt;Moving the checkout under &lt;code&gt;$HOME&lt;/code&gt; fixed it. If a bind mount looks empty inside a colima&lt;br&gt;
container, check the VM's mount list before you check anything else.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making it speak without a TTS vendor
&lt;/h2&gt;

&lt;p&gt;Attendee's documented endpoint for speaking is &lt;code&gt;/speech&lt;/code&gt;, and the source told me it was a dead&lt;br&gt;
end: &lt;em&gt;"Check for Google TTS credentials. This is currently the only supported text-to-speech&lt;br&gt;
provider."&lt;/em&gt; That meant a GCP project, a service account, and a billing profile — for something&lt;br&gt;
I'd already solved elsewhere.&lt;/p&gt;

&lt;p&gt;But the URL list had another entry, and it doesn't care where the bytes came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-codec:a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;libmp3lame&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-b:a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;128k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/bots/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bot_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/output_audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio/mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;
&lt;span class="c1"&gt;# 200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MediaBlob.VALID_AUDIO_CONTENT_TYPES&lt;/code&gt; is &lt;code&gt;[('audio/mp3', 'MP3 Audio')]&lt;/code&gt;. Generate audio&lt;br&gt;
anywhere, convert, POST. The entire Google Cloud dependency left the critical path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why my "conversation" was a bot talking to itself
&lt;/h2&gt;

&lt;p&gt;I ran a live loop: poll the transcript, generate a reply, speak it, repeat. Then I read the log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;→ [AI participant] ...how much time has passed, you don't know?
  ← replied in 3.82s
→ [AI participant] I don't know.
  ← replied in 7.07s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The bot's own speech landed in Meet's caption stream, and the loop read it back as new input.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That alone is an obvious bug. The part I didn't expect was the collateral damage. I had a&lt;br&gt;
cooldown so the bot wouldn't talk over people — and the self-loop kept that cooldown permanently&lt;br&gt;
hot, so it started dropping the &lt;em&gt;human's&lt;/em&gt; turns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(skipped: cooldown 3.0s) [Human] I measured that one at about 7 seconds.
(skipped: cooldown 0.0s) [Human] so you'd need it to say "got it, thinking" immediately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human on the call said "this isn't connecting." They were right, and not for the reason&lt;br&gt;
either of us assumed. The fix is one line — filter transcript rows by &lt;code&gt;speaker_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The lesson is not the line. It's that &lt;strong&gt;a self-loop does not present as an error. It presents as&lt;br&gt;
poor conversational quality&lt;/strong&gt;, and you will debug the wrong layer for as long as you trust your&lt;br&gt;
impression of the conversation over the log.&lt;/p&gt;
&lt;h2&gt;
  
  
  The number that changed my mind
&lt;/h2&gt;

&lt;p&gt;With the loop fixed, latency across seven consecutive turns looked 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;6.16  3.82  7.07  6.44  5.31  4.84  8.77   (seconds)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Median 6.16s. A single request outside the loop was &lt;strong&gt;1.77s&lt;/strong&gt; to first audio chunk (110 chunks,&lt;br&gt;
5.32s of speech). The ~4s gap is connection setup, repeated every turn.&lt;/p&gt;

&lt;p&gt;So I had a latency problem, and I started planning fixes for it. Then I looked at what the bot&lt;br&gt;
was actually &lt;em&gt;saying&lt;/em&gt;, and realized latency was the wrong problem.&lt;/p&gt;

&lt;p&gt;I ran a controlled comparison. Same model (&lt;code&gt;gemini-2.5-flash&lt;/code&gt;), same question, same prompt&lt;br&gt;
scaffold. One variable: &lt;strong&gt;2,545 characters of context&lt;/strong&gt; appended to the system instruction —&lt;br&gt;
a short digest of ongoing work, competitor pricing, and our own pricing.&lt;/p&gt;

&lt;p&gt;The question was the kind anyone gets asked in a strategy meeting:&lt;br&gt;
&lt;em&gt;"What is our value again — what makes us different from these people?"&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;Latency&lt;/th&gt;
&lt;th&gt;Shape of the answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.80s&lt;/td&gt;
&lt;td&gt;A deflection. "There's still room for discussion — how about we set up a session to align our understanding?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;With context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.68s&lt;/td&gt;
&lt;td&gt;A position with specifics: named our pricing model, cited what the competitor comparison had shown, stated where we differ operationally.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Latency was identical.&lt;/strong&gt; Within noise. The model was the same. The only thing that changed was&lt;br&gt;
what it knew.&lt;/p&gt;

&lt;p&gt;The first answer is what you say to buy time. The second can be argued with — someone can check&lt;br&gt;
the pricing claim, disagree with the positioning, push back on the comparison. That is the&lt;br&gt;
difference between an assistant and a stalling tactic.&lt;/p&gt;

&lt;p&gt;Note the shape of the failure: &lt;strong&gt;ask a deep question, get a shallow answer.&lt;/strong&gt; Not a wrong answer —&lt;br&gt;
a &lt;em&gt;smooth&lt;/em&gt; one, with nothing in it. That's the tell. It reads as "the model isn't good enough at&lt;br&gt;
reasoning" when it's actually "the model has nothing to reason over."&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Measure what your agent knows before you upgrade what it thinks with.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reflex when an agent underperforms is to reach for a bigger model. That reflex has a cost —&lt;br&gt;
more money, more latency, sometimes a migration — and in my case it would have bought nothing,&lt;br&gt;
because the agent was reasoning from an empty context and doing that competently.&lt;/p&gt;

&lt;p&gt;A cheap test: take one question your agent handled badly. Paste the context a &lt;em&gt;human colleague&lt;/em&gt;&lt;br&gt;
would have had in their head when answering it. Re-run with the same model. If the answer&lt;br&gt;
transforms, your problem was never the model.&lt;/p&gt;

&lt;p&gt;Which also means the build work isn't model selection. It's the layer that decides what your&lt;br&gt;
agent knows at the moment it's asked — and that layer has its own problems, starting with what&lt;br&gt;
you're allowed to put in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free tier has a price, and it isn't money
&lt;/h2&gt;

&lt;p&gt;From Gemini's pricing page, on the Live API audio model row:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Used to improve our products: Yes&lt;/code&gt; (free tier) / &lt;code&gt;No&lt;/code&gt; (paid tier)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For meetings containing customer names, that's a real decision. So the context layer needs a&lt;br&gt;
filter in front of it: a deterministic masker — no model judgment, just a lookup table.&lt;/p&gt;

&lt;p&gt;Here's the part that surprised me. My hand-built table of 82 names, generated from canonical&lt;br&gt;
company records, scored &lt;strong&gt;1 replacement across 86 transcripts — and it was a false positive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Speech transcripts don't contain canonical company names. They contain what people &lt;em&gt;say&lt;/em&gt;, and&lt;br&gt;
speech recognition mangles proper nouns. One person's surname never appeared in its correct form&lt;br&gt;
in a 36,240-character transcript.&lt;/p&gt;

&lt;p&gt;Switching to an existing alias registry — built for a different purpose, listing the terms people&lt;br&gt;
actually use out loud — changed everything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Replacements&lt;/th&gt;
&lt;th&gt;Files with ≥1 hit (of 86)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hand-built, 82 canonical names&lt;/td&gt;
&lt;td&gt;1 (false positive)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry &lt;code&gt;strong&lt;/code&gt; terms&lt;/td&gt;
&lt;td&gt;3,324&lt;/td&gt;
&lt;td&gt;41 (47%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;strong&lt;/code&gt; + a dedicated &lt;code&gt;mask&lt;/code&gt; field&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,071&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;58 (67%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Measured across 1,728,094 characters. &lt;strong&gt;If you're redacting speech, build your table from spoken&lt;br&gt;
forms, not from your database.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on running this in Japanese
&lt;/h3&gt;

&lt;p&gt;I expected the language to be the hard part. It wasn't — not where I expected it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt; Gemini Live took &lt;code&gt;language_code: "ja-JP"&lt;/code&gt; and produced speech a native speaker&lt;br&gt;
in the call judged natural on the first attempt. Google Meet's own captions&lt;br&gt;
(&lt;code&gt;google_meet_language: "ja-JP"&lt;/code&gt;) transcribed conversational Japanese cleanly enough to feed&lt;br&gt;
straight into the model. Neither needed tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; proper nouns, and only proper nouns — the same failure the masking numbers above&lt;br&gt;
already show. If your pipeline keys on names (redaction, entity linking, CRM lookup), that's&lt;br&gt;
where the work is, and it's a per-name problem you close by growing a table, not a model problem&lt;br&gt;
you fix by upgrading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One structural difference if you've only done this in English:&lt;/strong&gt; Japanese has no spaces between&lt;br&gt;
words, so substring matching has no natural boundaries. My masker replaced a fragment inside an&lt;br&gt;
unrelated word on its first run, because two characters of a company name happened to sit inside&lt;br&gt;
a common noun. In English a &lt;code&gt;\b&lt;/code&gt; word boundary catches that for free. Here you need explicit&lt;br&gt;
negative lookarounds per term, or a real tokenizer.&lt;/p&gt;

&lt;p&gt;So: viable, and cheaper than I expected. Budget your effort for the name layer, not the speech&lt;br&gt;
layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;You need about an hour and no paid accounts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;brew install colima docker docker-compose docker-buildx&lt;/code&gt;, start colima with
&lt;code&gt;--vm-type=vz --vz-rosetta&lt;/code&gt;, and verify &lt;code&gt;docker run --platform=linux/amd64 alpine uname -m&lt;/code&gt;
prints &lt;code&gt;x86_64&lt;/code&gt; before building anything.&lt;/li&gt;
&lt;li&gt;Clone &lt;a href="https://github.com/attendee-labs/attendee" rel="noopener noreferrer"&gt;Attendee&lt;/a&gt; &lt;strong&gt;under &lt;code&gt;$HOME&lt;/code&gt;&lt;/strong&gt;, then follow its
dev flow: &lt;code&gt;docker compose -f dev.docker-compose.yaml build&lt;/code&gt;, generate &lt;code&gt;.env&lt;/code&gt; via &lt;code&gt;init_env.py&lt;/code&gt;,
&lt;code&gt;up&lt;/code&gt;, &lt;code&gt;migrate&lt;/code&gt;, create an account at &lt;code&gt;localhost:8000&lt;/code&gt;, and confirm it with the link printed to
the server log. The API key comes from the UI afterward.&lt;/li&gt;
&lt;li&gt;Join a meeting with &lt;code&gt;POST /api/v1/bots&lt;/code&gt;, then push any mp3 through &lt;code&gt;/output_audio&lt;/code&gt; and confirm
you hear it. Do this before writing any agent logic; it isolates the plumbing from your code.&lt;/li&gt;
&lt;li&gt;Now the real test: take one question your agent answered badly. Add the context a colleague
would have had. Re-run with the &lt;strong&gt;same&lt;/strong&gt; model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If step 4 changes the answer, you know where your next week goes.&lt;/p&gt;

&lt;p&gt;What did your version of step 4 tell you? I'd like to know whether the gap shows up outside my&lt;br&gt;
setup.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;This article was drafted with AI assistance. All measurements in it were produced on my own&lt;br&gt;
machine, and responsibility for their accuracy rests with me.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>docker</category>
    </item>
    <item>
      <title>My First Day With OpenTelemetry: Spans That Arrive but Measure Nothing</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Fri, 07 Aug 2026 15:00:36 +0000</pubDate>
      <link>https://dev.to/nomurasan/my-first-day-with-opentelemetry-spans-that-arrive-but-measure-nothing-k3p</link>
      <guid>https://dev.to/nomurasan/my-first-day-with-opentelemetry-spans-that-arrive-but-measure-nothing-k3p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How this piece came together.&lt;/strong&gt; A Zenn writing contest put OpenTelemetry back on my radar, and this is the record of learning it by doing — picking it up, hitting confusion, and working through the side-questions with an AI while writing it up. Every command, number, and output shown below was run and confirmed on my own machine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I touched OpenTelemetry for the first time today. I knew the name; I'd never installed it. This is the record of adding instrumentation to a piece of my own code and seeing what actually showed up.&lt;/p&gt;

&lt;p&gt;The short version: &lt;strong&gt;the first version of my instrumentation didn't work.&lt;/strong&gt; All 15 spans arrived cleanly, with the right names and the right attributes, and they lined up in the Jaeger UI exactly as expected. I was about to call it done — until I looked at the actual numbers. The parent span read 1,006 ms. The 15 child spans summed to &lt;strong&gt;0.085 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The missing ~1,000 ms wasn't recorded anywhere. No error, either.&lt;/p&gt;

&lt;p&gt;The cause was how I'd written it. I ran the work, then &lt;strong&gt;built spans while reading the results that came back&lt;/strong&gt;. The child spans were timing "how fast the loop that reads results runs," not the checks themselves.&lt;/p&gt;

&lt;p&gt;I rewrote the same logic to wrap the actual execution instead, and the same 15 children summed to &lt;strong&gt;1,183 ms&lt;/strong&gt;. Span count, names, attributes — none of that changed.&lt;/p&gt;

&lt;p&gt;What follows is how I got there, and then — after I thought I had a way to tell a "healthy" trace from a broken one — how I broke that judgment method too, and fixed it. Some of this will be obvious to anyone who already knows this stuff; I'm writing it down as a record of what a first-timer actually gets wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment: A Docker-less Local Setup
&lt;/h2&gt;

&lt;p&gt;I don't have Docker, podman, or colima installed. Most OpenTelemetry getting-started guides assume &lt;code&gt;docker compose&lt;/code&gt;, but both the Collector and Jaeger ship as plain binaries, so this works without any of that.&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;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; jaeger.tar.gz &lt;span class="se"&gt;\&lt;/span&gt;
    https://github.com/jaegertracing/jaeger/releases/download/v2.20.0/jaeger-2.20.0-darwin-arm64.tar.gz
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;tar &lt;/span&gt;xzf jaeger.tar.gz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./jaeger-2.20.0-darwin-arm64/jaeger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jaeger v2 has a built-in OTLP receiver, so it listens on port 4317 and serves the UI on 16686.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; http://localhost:16686/
&lt;span class="go"&gt;200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sending side is Python, with two dependencies.&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;uv add opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is a setup for local experimentation, not a recommended production configuration.&lt;/strong&gt; In production the default path is OTLP → Collector → backend. Putting a Collector in front gets you batching, retries, sampling, and the ability to swap destinations without the app ever knowing where the data actually goes. None of that is needed for what this article covers, so I skipped it — but please don't read "Jaeger alone was enough for me today" as "Jaeger alone is enough."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Measured
&lt;/h2&gt;

&lt;p&gt;A pre-publish check for blog articles. Before an article goes out, it runs checks — topic classification, leaked sensitive information, tag rules, posting-interval limits, and so on — and stops publication if anything trips. It's Python, and calling &lt;code&gt;run_gates(platform, article_path)&lt;/code&gt; returns a list of check results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bad Example: Building Spans After the Result Comes Back
&lt;/h2&gt;

&lt;p&gt;Here's what I wrote first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish gate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_gates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qiita&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# everything actually runs here
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                        &lt;span class="c1"&gt;# this just reads the finished results
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks reasonable at a glance. There's a span per check, and each one carries a name and an attribute. And Jaeger did show 15 child spans, right on cue.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;run_gates&lt;/code&gt; is completely finished by line 2. Everything from line 3 down is just reading results that already exist. What the span wraps is "read one result out of a list," not the check that produced it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bad example: spans built after the fact
  parent: 1006.0 ms / sum of 15 children: 0.085 ms
    0.046 ms  check: topic_class_qiita
    0.006 ms  check: sensitive_identifiers_qiita
    0.004 ms  check: unsourced_experience_qiita
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every child comes in under 0.05 ms, and the parent is a full second. &lt;strong&gt;Roughly 1,006 ms isn't recorded in any span at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What makes this dangerous is that nothing throws an error. The SDK exports fine, Jaeger renders fine, and the trace looks complete on the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Good Example: Wrapping the Actual Execution
&lt;/h2&gt;

&lt;p&gt;The fix is to start the span inside the function that actually runs, and keep it open until that function returns. For code I own, the natural way to do that is a decorator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deco&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nd"&gt;@functools.wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;deco&lt;/span&gt;

&lt;span class="nd"&gt;@traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check: authority verification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_authority&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing exceptions through &lt;code&gt;record_exception&lt;/code&gt; and &lt;code&gt;set_status&lt;/code&gt;, instead of swallowing them, is part of what makes this a workable minimum. Measuring three checks with this shape, the children summed to 100.0% of the parent.&lt;/p&gt;

&lt;p&gt;This particular task came with the constraint "don't touch the production code," so instead I swapped in a wrapped version of the function that runs a single check, at runtime, just for the measurement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;orig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gate_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_run_check&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;orig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# execution happens inside the span
&lt;/span&gt;        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;

&lt;span class="n"&gt;gate_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_run_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;
&lt;span class="nf"&gt;run_gates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qiita&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gate_runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_run_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This kind of runtime swap is a temporary measurement device, not something to leave in place.&lt;/strong&gt; Its blast radius is hard to reason about, it's easy to forget to restore, and it breaks if something else swaps the same function mid-flight under concurrency. For ongoing measurement, add a decorator instead — or, if the target is a well-known library, lean on automatic instrumentation via &lt;code&gt;opentelemetry-instrument&lt;/code&gt;. That said, automatic instrumentation only covers libraries it already has support for; &lt;strong&gt;your own functions aren't covered&lt;/strong&gt;, so those still need manual instrumentation either way.&lt;/p&gt;

&lt;p&gt;Re-measuring the same article, the same checks, the same span names, gives this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;good example: wrapping the execution
  parent: 1221.2 ms / sum of 15 children: 1183.058 ms
    644.6 ms  check: publish_authority_qiita
    538.4 ms  check: qiita_pre_publish_check_cli
      0.015 ms  check: topic_class_qiita
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parent at 1,221 ms against a child sum of 1,183 ms. The 38 ms gap is explainable — result assembly and similar overhead — and stays in a reasonable range.&lt;/p&gt;

&lt;p&gt;And for the first time, the contents were actually visible. Of that roughly one second, 644 ms was an authority check for publishing, and 538 ms was a call into an existing check script. Together those two account for 97% of the total — a number nobody had before instrumentation existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Judgment Method: My First Attempt Broke Under Parallelism
&lt;/h2&gt;

&lt;p&gt;Both traces have the same span count, the same names, the same attributes. Side by side in the Jaeger UI, unless you already know what you're looking for, you won't spot the difference. I wanted a number to judge by.&lt;/p&gt;

&lt;p&gt;My first idea was: sum the child span durations, divide by the parent. On the two examples above, that separates cleanly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bad example: spans built after the fact   0.0%
good example: wrapping the execution      96.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I thought that settled it — until I ran it on parallel execution, and it broke.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run three checks in parallel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;))&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;parent 301 ms / sum of 3 children 903 ms
naive sum ÷ parent = 299.8%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With three things running at once, a plain sum comes out to roughly three times wall-clock time. &lt;strong&gt;Building the metric from a single sequential example was the mistake.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix is to collapse overlapping intervals before comparing. Sort the intervals and merge them — no new dependency needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;union_us&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sum the child spans&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; occupied intervals, with overlaps removed (microseconds).

    Clips to the parent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s own time window — otherwise a child that spills
    outside the parent can push the result past 100%.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;cur_s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cur_s&lt;/span&gt;
            &lt;span class="n"&gt;cur_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur_e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;cur_e&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cur_s&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That took a detour through units. I had another model review this article's draft, and it flagged that &lt;code&gt;startTime&lt;/code&gt; and &lt;code&gt;duration&lt;/code&gt; are nanoseconds by spec, so naming the function &lt;code&gt;union_us&lt;/code&gt; (microseconds) and converting to milliseconds was off by a factor of 1,000. It sounded plausible enough that I nearly "fixed" it — but I &lt;strong&gt;checked first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The parent span's &lt;code&gt;duration&lt;/code&gt; for three &lt;code&gt;asyncio.sleep(0.3)&lt;/code&gt; calls run in parallel was &lt;code&gt;301311&lt;/code&gt;. As microseconds, that's 301.3 ms. As nanoseconds, it would be 0.3 ms. The code actually waits 300 ms, so &lt;strong&gt;microseconds is correct&lt;/strong&gt; — the review comment was the one that was wrong.&lt;/p&gt;

&lt;p&gt;When you're new to something and someone who sounds like they know it tells you "that's wrong," the instinct is to just go fix it. But running one case with a known, expected value and checking it against the output settles it in seconds — faster than tracking down the spec. And getting the order of magnitude wrong doesn't leave an obvious trace either: the ratio still looks correct on its face, so nothing points back to the mistake later.&lt;/p&gt;

&lt;p&gt;Running all three cases through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publish gate (decorator version)     naive sum  100.0% / union 100.0%
good example: wrapping the execution naive sum   96.9% / union  96.9%
running three checks in parallel     naive sum  299.8% / union 100.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The union stays within 100% even under parallel execution. Fetching the trace looked like this. The same service name also returns past runs, so &lt;strong&gt;remember to take only the most recent one&lt;/strong&gt; — I skipped that at first and got a "good example" that read 0.0%, because a pre-fix run was still mixed into the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:16686/api/traces?service=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;limit=20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;root_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;references&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;root_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;root_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;kids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;references&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;union / parent = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;union_us&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Far Should I Trust This Metric?
&lt;/h2&gt;

&lt;p&gt;A low number doesn't, by itself, mean "the instrumentation is broken." It also comes out low in these cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The parent is carrying I/O wait.&lt;/strong&gt; If waiting on an external API's response counts toward the parent's time and the child spans sit outside that wait, an uncovered interval remains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some work was never turned into a child span.&lt;/strong&gt; Data transformation, cache checks — anything in between that wasn't instrumented shows up only in the parent's time, never in a child's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The context broke across an async boundary.&lt;/strong&gt; Cross a thread or an &lt;code&gt;asyncio&lt;/code&gt; boundary without care and the parent-child link doesn't survive, so that work never gets tallied as a child at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this metric isn't "proof the instrumentation is broken." It's closer to &lt;strong&gt;a prompt to go find out why&lt;/strong&gt;, when a number looks off — not an established practice, just something I put together because I ran into this exact problem today. If it comes out near 0% and there's no I/O wait to explain it, the first thing to suspect is how the spans were built. That's exactly what happened to me here.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Have to Connect Things After the Fact
&lt;/h2&gt;

&lt;p&gt;There are legitimate designs where a result arrives somewhere else, at some other time — batch jobs, async queues. OpenTelemetry has two mechanisms for this, but &lt;strong&gt;they do different jobs&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Span Link&lt;/strong&gt;: connects a span in another trace in as a reference. Useful for a causal-but-not-parent-child relationship — "the work that got enqueued" and "the work that consumed it," for example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event&lt;/strong&gt;: marks a specific point in time inside a span. Records something that happened along the way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing to watch for: &lt;strong&gt;neither one stands in for measuring duration.&lt;/strong&gt; An Event is a point, not an interval, so it can't be used to split a duration after the fact. If you want to measure time, you have to put a span where the work actually executes. A Link is for reconnecting causality after the fact — reconnecting the causal chain doesn't reconstruct the timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Article Doesn't Cover
&lt;/h2&gt;

&lt;p&gt;I only measured a single local process, so this leaves out everything production actually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sampling.&lt;/strong&gt; Sending every span overwhelms the backend at scale; head-based vs. tail-based sampling is a real decision to make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context propagation.&lt;/strong&gt; Cross a thread or an &lt;code&gt;asyncio&lt;/code&gt; boundary carelessly and &lt;code&gt;contextvars&lt;/code&gt; breaks, severing the parent-child relationship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running a Collector.&lt;/strong&gt; Batching, retries, splitting destinations — the production default, and I skipped it entirely here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend persistence and auth.&lt;/strong&gt; I used a local, unauthenticated Jaeger instance as-is.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Building spans after processing finishes makes the duration vanish without an error.&lt;/strong&gt; The spans still arrive, and the names and attributes are still correct.&lt;/li&gt;
&lt;li&gt;Wrapping the actual execution surfaces the real numbers. The same target went from 0.085 ms to 1,183 ms without any change in span count, names, or attributes.&lt;/li&gt;
&lt;li&gt;For ongoing measurement, a decorator or automatic instrumentation seems to be the right shape. A runtime swap is a temporary investigation tool, not something to keep.&lt;/li&gt;
&lt;li&gt;The judgment metric I ended up using is &lt;strong&gt;union of child spans ÷ parent&lt;/strong&gt;. A naive sum hit 299.8% under parallel execution and was unusable.&lt;/li&gt;
&lt;li&gt;A low number means "go find the reason," not "it's broken." I/O waits and uninstrumented sections pull it down too.&lt;/li&gt;
&lt;li&gt;Link and Event, for connecting things after the fact, exist for causality and points in time — &lt;strong&gt;neither one measures duration&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding instrumentation makes it easy to relax, just because you "added it." But a span arriving and a span measuring the thing you actually wanted to measure turned out to be two different things. Even the metric I used to judge that difference was wrong on the first pass — built from a single example that didn't generalize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limits of This Article
&lt;/h2&gt;

&lt;p&gt;I wrote this without fully understanding the tool — reading the official docs and other people's write-ups, running things locally to check as I went. I expect there are places here that depart from normal practice.&lt;/p&gt;

&lt;p&gt;What I'm least confident about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "union of child spans ÷ parent" metric I used for judgment is something I built because I ran into this exact problem today — it isn't a general convention. There may be a more straightforward way to check this.&lt;/li&gt;
&lt;li&gt;I only looked at three ways to add instrumentation: a decorator, a runtime swap, and automatic instrumentation. Real-world practice surely has other patterns.&lt;/li&gt;
&lt;li&gt;I only measured a single process — sequential, and &lt;code&gt;asyncio&lt;/code&gt; parallelism. Threads, multiple processes, and distributed environments are a different story entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something here is off, I'd like to know. Every number and every piece of code shown was run locally and the output checked against what's printed, so if you reproduce it and get something different, that difference is the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/languages/python/instrumentation/" rel="noopener noreferrer"&gt;OpenTelemetry Python — Instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/languages/python/automatic/" rel="noopener noreferrer"&gt;OpenTelemetry Python — Automatic Instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/concepts/sampling/" rel="noopener noreferrer"&gt;OpenTelemetry — Sampling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jaegertracing.io/docs/latest/getting-started/" rel="noopener noreferrer"&gt;Jaeger — Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bugsmash</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>python</category>
    </item>
    <item>
      <title>~/.ssh/config's Permission Check: When OpenSSH Enforces It, and What Changes Between ssh -F and Include</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:28:55 +0000</pubDate>
      <link>https://dev.to/nomurasan/sshconfigs-permission-check-when-openssh-enforces-it-and-what-changes-between-ssh-f-and-2knj</link>
      <guid>https://dev.to/nomurasan/sshconfigs-permission-check-when-openssh-enforces-it-and-what-changes-between-ssh-f-and-2knj</guid>
      <description>&lt;p&gt;A draft I was reviewing claimed that OpenSSH's client doesn't enforce permissions on &lt;code&gt;~/.ssh/config&lt;/code&gt;. The evidence was a &lt;code&gt;chmod&lt;/code&gt; sweep — &lt;code&gt;600&lt;/code&gt;, &lt;code&gt;644&lt;/code&gt;, &lt;code&gt;664&lt;/code&gt;, &lt;code&gt;666&lt;/code&gt; — with every run exiting &lt;code&gt;0&lt;/code&gt;. The commands were real. They ran. The output was genuine. The conclusion was backwards: permissions are enforced.&lt;/p&gt;

&lt;p&gt;Re-running doesn't catch this one. Run the same commands again and you get &lt;code&gt;exit=0&lt;/code&gt; every time, forever. What was broken wasn't the output — it was the path that produced it. Point &lt;code&gt;-F&lt;/code&gt; at a config file directly and OpenSSH's permission check never runs at all. Four runs down a path that skips the check got read as four pieces of evidence that the check doesn't exist.&lt;/p&gt;

&lt;p&gt;Even when a command and its output are genuine and reproducible, that still does not show that the test exercised the path it claims to — and that gap survives a re-run. What follows is four checks that caught both failures above, roughly cheapest first: a contradiction check, an existence check, a positive control, and an ablation test. All four run inside &lt;code&gt;ssh&lt;/code&gt; and a terminal. This isn't a complete taxonomy of measurement errors — it's the cheap heuristics that happened to work on these two cases; what it doesn't cover comes at the end.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on how this was checked.&lt;/strong&gt; Every command and output below was re-run in a disposable scratch directory immediately before publishing and matched verbatim, with one substitution: the scratch directory's absolute path was replaced with the placeholder &lt;code&gt;/tmp/ssh-demo&lt;/code&gt; for readability. The numbers, output, and line order are otherwise the run's own. None of the real &lt;code&gt;~/.ssh&lt;/code&gt; on this machine was touched.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ssh
/usr/bin/ssh

&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-V&lt;/span&gt;
OpenSSH_10.2p1, LibreSSL 3.3.6

&lt;span class="nv"&gt;$ &lt;/span&gt;codesign &lt;span class="nt"&gt;-dv&lt;/span&gt; /usr/bin/ssh 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep &lt;/span&gt;Identifier
&lt;span class="nv"&gt;Identifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;com.apple.ssh
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Every command below runs this same &lt;code&gt;/usr/bin/ssh&lt;/code&gt;. How the default config path — &lt;code&gt;~/.ssh/config&lt;/code&gt; itself — was safely tested, and the limits that testing ran into, are covered in a later section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Check 1 — &lt;code&gt;ssh -G myhost&lt;/code&gt; Returns &lt;code&gt;port 2200&lt;/code&gt;: Two Explanations for the Same Result
&lt;/h2&gt;

&lt;p&gt;The cheapest check needs no terminal at all. Read a single passage for one observed result that gets explained two incompatible ways.&lt;/p&gt;

&lt;p&gt;Take this config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host myhost
    HostName 127.0.0.1
    User specific-user
    Port 2200

Match host myhost user specific-user
    Port 2300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_test &lt;span class="nt"&gt;-G&lt;/span&gt; specific-user@myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^port '&lt;/span&gt;
port 2200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Match&lt;/code&gt;'s &lt;code&gt;Port 2300&lt;/code&gt; doesn't win here. A draft I reviewed offered two different reasons for this, in the same section. First: &lt;code&gt;Port 2200&lt;/code&gt; locks in before &lt;code&gt;Match&lt;/code&gt; is evaluated, so first-value-wins discards &lt;code&gt;2300&lt;/code&gt;. Second: &lt;code&gt;Match user&lt;/code&gt; is checked against the OS login account, and since neither that account nor the &lt;code&gt;Host&lt;/code&gt; block's &lt;code&gt;User specific-user&lt;/code&gt; has taken effect yet, &lt;code&gt;Match&lt;/code&gt; never fires in the first place.&lt;/p&gt;

&lt;p&gt;Those can't both be true. First-value-wins is a rule about what happens once &lt;code&gt;Match&lt;/code&gt;'s condition is already satisfied. If the second explanation is right, the condition was never satisfied, and first-value-wins never got a turn to do anything. If the first explanation is right, &lt;code&gt;Match&lt;/code&gt; must have fired — which is exactly what the second explanation denies.&lt;/p&gt;

&lt;p&gt;Knowing they can't both be true doesn't say which one is. The single &lt;code&gt;port 2200&lt;/code&gt; above is consistent with either story: &lt;code&gt;Match&lt;/code&gt; firing and losing to first-value-wins looks identical, from the outside, to &lt;code&gt;Match&lt;/code&gt; never firing at all. Telling them apart needs a config built so the two predictions come apart.&lt;/p&gt;

&lt;p&gt;A narrower config decides which half is wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host myhost
    HostName 127.0.0.1
    User specific-user

Match user specific-user
    Port 2300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_user &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(user|port) '&lt;/span&gt;
user specific-user
port 2300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Match user specific-user&lt;/code&gt; succeeds, and the shell running this command is logged in under a different OS account entirely, not &lt;code&gt;specific-user&lt;/code&gt;. The only way &lt;code&gt;Match&lt;/code&gt; fires here is by reading &lt;code&gt;specific-user&lt;/code&gt; back out of the &lt;code&gt;Host&lt;/code&gt; block's &lt;code&gt;User&lt;/code&gt; line, not the OS account. The second explanation doesn't survive this — for this config.&lt;/p&gt;

&lt;p&gt;Calling it flatly wrong would overreach, though. &lt;code&gt;Match user&lt;/code&gt; reads a resolved remote-user value, and when nothing has resolved one yet, that value falls back to the OS login name by default. A config with no explicit &lt;code&gt;User&lt;/code&gt; line shows the fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host myhost
    HostName 127.0.0.1

Match user deploy
    Port 2400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_localdefault &lt;span class="nt"&gt;-G&lt;/span&gt; deploy@myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(user|port) '&lt;/span&gt;
user deploy
port 2400

&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_localdefault &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^port '&lt;/span&gt;
port 22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Match&lt;/code&gt; fires only when the command line supplies &lt;code&gt;deploy@&lt;/code&gt;. Called as plain &lt;code&gt;myhost&lt;/code&gt;, the unresolved &lt;code&gt;user&lt;/code&gt; value falls back to whoever's logged in locally — not &lt;code&gt;deploy&lt;/code&gt; — so &lt;code&gt;Match&lt;/code&gt; doesn't fire. The second explanation describes a real fallback path; it's just misapplied to the original config, where the &lt;code&gt;Host&lt;/code&gt; block already resolves &lt;code&gt;User&lt;/code&gt; to &lt;code&gt;specific-user&lt;/code&gt; before &lt;code&gt;Match&lt;/code&gt; is evaluated, leaving no unresolved value for a local login name to fall back into.&lt;/p&gt;

&lt;p&gt;There's a keyword for exactly the mechanism the second explanation was reaching for: &lt;code&gt;Match localuser&lt;/code&gt; matches the OS login account directly, unconditionally. &lt;code&gt;Match user&lt;/code&gt; doesn't — it matches a resolved &lt;em&gt;remote&lt;/em&gt;-user value that only falls back to the OS login name once nothing else, like a &lt;code&gt;Host&lt;/code&gt; block's &lt;code&gt;User&lt;/code&gt; line, has already set it. &lt;code&gt;readconf.c&lt;/code&gt;'s &lt;code&gt;match_cfg_line()&lt;/code&gt; recomputes that value fresh every time it starts evaluating a &lt;code&gt;Match&lt;/code&gt; line: &lt;code&gt;ruser = options-&amp;gt;user == NULL ? pw-&amp;gt;pw_name : options-&amp;gt;user;&lt;/code&gt; — whatever a prior &lt;code&gt;Host&lt;/code&gt; block already put in &lt;code&gt;options-&amp;gt;user&lt;/code&gt; wins; the OS login name is only what's left when nothing did. The second explanation wasn't describing a mechanism that doesn't exist; it was describing &lt;code&gt;localuser&lt;/code&gt;'s behavior and attaching it to &lt;code&gt;user&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That leaves the first explanation standing for the original config — Check 1 can only show that the draft contradicted itself, not settle which half, if either, was actually right in general. Check 4 comes back to close that out.&lt;/p&gt;

&lt;p&gt;Nothing about this is specific to &lt;code&gt;ssh_config&lt;/code&gt;. The same shape shows up wherever a write-up explains one failure two ways: a CI failure blamed on both "the code regressed" and "the test environment never came up" in the same paragraph, a support ticket blamed on both a caching bug and a race condition. In each case the two stories can't both be the reason, and the write-up that offers both hasn't actually settled which one is.&lt;/p&gt;

&lt;p&gt;This check catches a draft that states two competing explanations for the same result in the same place; it does nothing for a draft that's wrong consistently. It costs zero commands, which is exactly why it's the first pass on anything that lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2 — &lt;code&gt;chmod 600/644/664/666&lt;/code&gt; All Exit 0 on &lt;code&gt;ssh -F&lt;/code&gt;: Looking for &lt;code&gt;Bad owner or permissions&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A different draft's permissions chapter made a clean claim: SSH's client doesn't enforce permissions on &lt;code&gt;~/.ssh/config&lt;/code&gt; — &lt;code&gt;600&lt;/code&gt;, &lt;code&gt;644&lt;/code&gt;, &lt;code&gt;664&lt;/code&gt;, &lt;code&gt;666&lt;/code&gt; all pass.&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="k"&gt;for &lt;/span&gt;m &lt;span class="k"&gt;in &lt;/span&gt;600 644 664 666&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt; config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%Lp'&lt;/span&gt; config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    /usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config &lt;span class="nt"&gt;-G&lt;/span&gt; myhost &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&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="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;600
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
644
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
664
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
666
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;stat&lt;/code&gt; runs right after &lt;code&gt;chmod&lt;/code&gt; here, rather than chaining &lt;code&gt;chmod &amp;amp;&amp;amp; ssh&lt;/code&gt; — that way a &lt;code&gt;chmod&lt;/code&gt; that silently failed can't produce a false pass.)&lt;/p&gt;

&lt;p&gt;The commands are real, they ran, the exit codes are genuine. "Not enforced" is still backwards. What catches this one: before accepting a negative claim — "not enforced," "not required," "not checked" — confirm the code that would do the enforcing exists in the binary at all.&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;strings /usr/bin/ssh | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Bad owner"&lt;/span&gt;
Bad owner or permissions on %s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's there. Finding nothing wouldn't have proven the check is absent — it could live in a shared library, or surface as a different message — but finding this string means "not enforced" needs more than the sweep above to stand. This costs one &lt;code&gt;strings&lt;/code&gt; call, cheap enough to run before accepting any negative conclusion: absent code kills the claim right here; present code is the cue for Check 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3 — &lt;code&gt;ssh -F&lt;/code&gt; and &lt;code&gt;Include&lt;/code&gt; Give Different Results: Adding the &lt;code&gt;chmod 664&lt;/code&gt; Case Into the Measurement
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;strings&lt;/code&gt; only shows that the check exists somewhere in the binary; it can't say which path reaches it. That needs the source, and the caveat from the note at the top applies here too: this is a static reading of &lt;code&gt;openssh-portable&lt;/code&gt; upstream, not a trace of what the local binary actually executes.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;ssh.c&lt;/code&gt;, &lt;code&gt;process_config_files()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strcasecmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;read_config_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SSHCONF_USERCONF&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_pass&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_FINAL&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;want_final_pass&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Can't open user config file %.100s: "&lt;/span&gt;
            &lt;span class="s"&gt;"%.100s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errno&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%s/%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pw_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_PATH_SSH_USER_CONFFILE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;read_config_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_CHECKPERM&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_USERCONF&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_pass&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_FINAL&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;want_final_pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="cm"&gt;/* Read systemwide configuration file after user config. */&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;read_config_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_PATH_HOST_CONFIG_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;final_pass&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_FINAL&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want_final_pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A file handed to &lt;code&gt;ssh&lt;/code&gt; via &lt;code&gt;-F&lt;/code&gt; only ever carries &lt;code&gt;SSHCONF_USERCONF&lt;/code&gt;. &lt;code&gt;SSHCONF_CHECKPERM&lt;/code&gt; — the flag that turns permission checking on — is set only on one call in the &lt;code&gt;else&lt;/code&gt; branch, the one that builds and reads the &lt;em&gt;default&lt;/em&gt; per-user &lt;code&gt;~/.ssh/config&lt;/code&gt; path; the systemwide config read right below it doesn't carry the flag either. The four-pattern sweep in Check 2 ran entirely on the &lt;code&gt;-F&lt;/code&gt; branch, which never sets &lt;code&gt;SSHCONF_CHECKPERM&lt;/code&gt; at all; it never had a chance to fail, independent of what the actual permissions were.&lt;/p&gt;

&lt;p&gt;One path that does set the flag is &lt;code&gt;Include&lt;/code&gt;. From &lt;code&gt;readconf.c&lt;/code&gt;, &lt;code&gt;Include&lt;/code&gt; handling inside &lt;code&gt;process_config_line_depth()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_config_file_depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_pathv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;original_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remote_command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_CHECKPERM&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oactive&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_NEVERMATCH&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;activep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want_final_pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every file pulled in through &lt;code&gt;Include&lt;/code&gt; gets &lt;code&gt;SSHCONF_CHECKPERM&lt;/code&gt; unconditionally. What the flag actually does, further down the same function, &lt;code&gt;read_config_file_depth()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;SSHCONF_CHECKPERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fstat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileno&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fstat %s: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errno&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_uid&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_uid&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;getuid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mode&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mo"&gt;022&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bad owner or permissions on %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-run the same permission sweep, but through a path that actually sets the flag: an outer file, held at &lt;code&gt;600&lt;/code&gt;, that &lt;code&gt;Include&lt;/code&gt;s an inner file whose permissions vary. Both &lt;code&gt;ssh&lt;/code&gt; calls below use &lt;code&gt;-T&lt;/code&gt; (it suppresses an unrelated pty warning and doesn't affect the port or permission-check results these checks read) and the same absolute paths for &lt;code&gt;-F&lt;/code&gt; and &lt;code&gt;Include&lt;/code&gt;. &lt;code&gt;stat&lt;/code&gt; runs immediately before each call, so the permission and path actually observed are the ones under test, not assumed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# outer.conf, permissions 600, fixed
Include /tmp/ssh-demo/inner.conf
&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;# inner.conf, permissions varied 600 -&amp;gt; 644 -&amp;gt; 664 -&amp;gt; 666
Host myhost
    HostName 127.0.0.1
    User specific-user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;perm.sh
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-demo
&lt;span class="k"&gt;for &lt;/span&gt;m &lt;span class="k"&gt;in &lt;/span&gt;600 644 664 666&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/inner.conf"&lt;/span&gt;
  &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%d %i %u %Lp %N'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/inner.conf"&lt;/span&gt;
  /usr/bin/ssh &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/inner.conf"&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; myhost &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  direct -F      exit=&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%d %i %u %Lp %N'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/inner.conf"&lt;/span&gt;
  /usr/bin/ssh &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/outer.conf"&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; myhost &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  via Include    exit=&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;bash perm.sh
16777230 296585647 503 600 /tmp/ssh-demo/inner.conf
  direct &lt;span class="nt"&gt;-F&lt;/span&gt;      &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
16777230 296585647 503 600 /tmp/ssh-demo/inner.conf
  via Include    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
16777230 296585647 503 644 /tmp/ssh-demo/inner.conf
  direct &lt;span class="nt"&gt;-F&lt;/span&gt;      &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
16777230 296585647 503 644 /tmp/ssh-demo/inner.conf
  via Include    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
16777230 296585647 503 664 /tmp/ssh-demo/inner.conf
  direct &lt;span class="nt"&gt;-F&lt;/span&gt;      &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
16777230 296585647 503 664 /tmp/ssh-demo/inner.conf
Bad owner or permissions on /tmp/ssh-demo/inner.conf
  via Include    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
16777230 296585647 503 666 /tmp/ssh-demo/inner.conf
  direct &lt;span class="nt"&gt;-F&lt;/span&gt;      &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
16777230 296585647 503 666 /tmp/ssh-demo/inner.conf
Bad owner or permissions on /tmp/ssh-demo/inner.conf
  via Include    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stat&lt;/code&gt;'s columns, in order: device, inode, owner uid, permissions, path. Across the 8 &lt;code&gt;stat&lt;/code&gt; calls immediately preceding each &lt;code&gt;ssh&lt;/code&gt; run, device and inode stayed constant and the owner uid matched this account's. That's as far as the observation goes — whether the file could have changed between &lt;code&gt;stat&lt;/code&gt; and &lt;code&gt;ssh&lt;/code&gt;'s own &lt;code&gt;open()&lt;/code&gt;, or whether both paths' open file descriptors actually resolved to that same device/inode, wasn't checked.&lt;/p&gt;

&lt;p&gt;The only edit made to this transcript for publication was substituting the working directory's absolute path with &lt;code&gt;/tmp/ssh-demo&lt;/code&gt;, as noted at the top. The numbers, output, and order are the run's own.&lt;/p&gt;

&lt;p&gt;Same file, same permissions, only the path differs. &lt;code&gt;-F&lt;/code&gt; direct passes all four permission levels; &lt;code&gt;Include&lt;/code&gt; fails at &lt;code&gt;664&lt;/code&gt; and &lt;code&gt;666&lt;/code&gt; with the exact string Check 2 found sitting in the binary. Because &lt;code&gt;664&lt;/code&gt; and &lt;code&gt;666&lt;/code&gt; disagree between the two paths on files that are otherwise identical, path is what's gating the check — and once a path does trigger it, group- or other-writable is what fails it.&lt;/p&gt;

&lt;p&gt;I stopped there. The default path itself, &lt;code&gt;~/.ssh/config&lt;/code&gt;, was never touched — the real &lt;code&gt;~/.ssh&lt;/code&gt; on this machine stayed out of scope for this kind of audit. Redirecting &lt;code&gt;$HOME&lt;/code&gt; to a scratch directory looks like a shortcut around that, but it doesn't work: the &lt;code&gt;else&lt;/code&gt; branch in &lt;code&gt;ssh.c&lt;/code&gt; builds the default path from &lt;code&gt;pw-&amp;gt;pw_dir&lt;/code&gt;, the account's actual home directory from &lt;code&gt;getpwuid()&lt;/code&gt;, and never reads &lt;code&gt;$HOME&lt;/code&gt;. That same branch always carries &lt;code&gt;SSHCONF_CHECKPERM&lt;/code&gt; regardless of which directory it ends up reading, so the source gives no reason to expect the default path at &lt;code&gt;664&lt;/code&gt;/&lt;code&gt;666&lt;/code&gt; to behave any differently from &lt;code&gt;inner.conf&lt;/code&gt; above — but that's a prediction from the source, not a run I have to show for it.&lt;/p&gt;

&lt;p&gt;The same pattern shows up outside SSH. In CI, plant a known break in the exact path a test claims to cover and check whether the intended assertion fails for the intended reason; adding one &lt;code&gt;assert false&lt;/code&gt; to force red only proves the test runner executes, not that the target path was ever reached. For a health check, take the monitored service down in an isolated environment and check whether the contract's failure signal — non-2xx, connection refused, timeout, whichever applies — actually fires; if it stays green, the thing being watched is a different process or a different environment. Mock-heavy tests invert this: with the real implementation swapped for a mock, breaking that implementation &lt;em&gt;should&lt;/em&gt; leave the test green, so the thing to break instead is the mock's return value or the contract behind it, or route through an integration test that exercises the real implementation. Same goal every time: plant a known failure and check it gets caught. If it doesn't, the measurement may never have reached the path it claims to test. A sweep with no case that has to fail can't tell a silent mechanism from an absent one — adding &lt;code&gt;664&lt;/code&gt;/&lt;code&gt;666&lt;/code&gt; through a path that actually sets the flag is what turns "everything passed" into a result that means something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirecting &lt;code&gt;$HOME&lt;/code&gt; Doesn't Move the Default Path — Measuring the Boundary Through &lt;code&gt;Include&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The previous section closed by leaving the default path as inference. Here's how far that can actually be pushed.&lt;/p&gt;

&lt;p&gt;The obvious next move looked like redirecting &lt;code&gt;$HOME&lt;/code&gt; to point the default path at an isolated environment:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/ssh-demo/isolated-home/.ssh
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/ssh-demo/isolated-home/.ssh/config
Host myhost
    HostName 127.0.0.1
    Port 2200

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /tmp/ssh-demo/isolated-home/.ssh
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /tmp/ssh-demo/isolated-home/.ssh/config
&lt;span class="nv"&gt;$ HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-demo/isolated-home /usr/bin/ssh &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(hostname|port) '&lt;/span&gt;
&lt;span class="nb"&gt;hostname &lt;/span&gt;myhost
port 22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither &lt;code&gt;HostName 127.0.0.1&lt;/code&gt; nor &lt;code&gt;Port 2200&lt;/code&gt; took effect. The file itself isn't broken — pointing &lt;code&gt;-F&lt;/code&gt; straight at it reads it fine:&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;$ HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-demo/isolated-home /usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; /tmp/ssh-demo/isolated-home/.ssh/config &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(hostname|port) '&lt;/span&gt;
&lt;span class="nb"&gt;hostname &lt;/span&gt;127.0.0.1
port 2200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop &lt;code&gt;-F&lt;/code&gt; and keep only the &lt;code&gt;$HOME&lt;/code&gt; redirect, and it's back to &lt;code&gt;hostname myhost&lt;/code&gt; (unsubstituted) and &lt;code&gt;port 22&lt;/code&gt; (the compiled-in default). &lt;code&gt;-vvv&lt;/code&gt; shows which file actually got opened:&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;$ HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-demo/isolated-home /usr/bin/ssh &lt;span class="nt"&gt;-vvv&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; myhost 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Reading configuration data"&lt;/span&gt;
debug1: Reading configuration data &lt;span class="nv"&gt;$REAL_HOME&lt;/span&gt;/.ssh/config
debug1: Reading configuration data /etc/ssh/ssh_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$REAL_HOME&lt;/code&gt; stands in for this machine's actual home directory, substituted for publication; the rest of the line is the run's own. The default path is built from &lt;code&gt;pw_dir&lt;/code&gt;, not from &lt;code&gt;$HOME&lt;/code&gt;, so redirecting the environment variable never moves it. The source explains why: the default path is assembled from &lt;code&gt;pw-&amp;gt;pw_dir&lt;/code&gt;, and that &lt;code&gt;pw&lt;/code&gt; comes from &lt;code&gt;getpwuid(getuid())&lt;/code&gt;, not from &lt;code&gt;getenv("HOME")&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* ssh.c, line 729 */&lt;/span&gt;
&lt;span class="n"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getpwuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getuid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="cm"&gt;/* ssh.c, lines 589-590, the `config == NULL` else branch */&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%s/%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pw_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_PATH_SSH_USER_CONFFILE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the default path's construction never reads &lt;code&gt;$HOME&lt;/code&gt;, giving it a &lt;code&gt;pw_dir&lt;/code&gt; different from the real one takes a different UID — which takes root, which this working environment doesn't have. Touching the real &lt;code&gt;~/.ssh&lt;/code&gt; on this machine was never on the table either. So the literal thing — set the default path to &lt;code&gt;664&lt;/code&gt; and watch it fail — wasn't measured here.&lt;/p&gt;

&lt;p&gt;What the source does confirm directly: reading &lt;code&gt;readconf.c&lt;/code&gt;, the default-path branch (the &lt;code&gt;else&lt;/code&gt; in &lt;code&gt;ssh.c&lt;/code&gt;) and the &lt;code&gt;Include&lt;/code&gt; branch both end up in the same &lt;code&gt;read_config_file()&lt;/code&gt;, hitting the same check (&lt;code&gt;if (flags &amp;amp; SSHCONF_CHECKPERM) { ... }&lt;/code&gt;, testing &lt;code&gt;sb.st_mode &amp;amp; 022&lt;/code&gt;). The only difference between the two callers is whether &lt;code&gt;SSHCONF_CHECKPERM&lt;/code&gt; gets set before getting there; once it's set, there's exactly one check to pass. The &lt;code&gt;Include&lt;/code&gt; measurement above exercises that shared check directly. It isn't a measurement of the default path itself, but it is a measurement of the exact code the default path relies on — a proxy, and one worth keeping distinct from a direct measurement.&lt;/p&gt;

&lt;p&gt;Three paths, side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;&lt;code&gt;SSHCONF_CHECKPERM&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;How it was tested&lt;/th&gt;
&lt;th&gt;&lt;code&gt;600&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;644&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;664&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;666&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-F&lt;/code&gt; direct&lt;/td&gt;
&lt;td&gt;not set&lt;/td&gt;
&lt;td&gt;measured directly, isolated environment&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;via &lt;code&gt;Include&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;measured directly, isolated environment&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bad owner...&lt;/code&gt; (255)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bad owner...&lt;/code&gt; (255)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;default path (no &lt;code&gt;-F&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;set (confirmed from source — same branch as &lt;code&gt;Include&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;can't be measured directly in this environment; &lt;code&gt;Include&lt;/code&gt;'s result stands in as a proxy&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;664&lt;/code&gt; and &lt;code&gt;666&lt;/code&gt; both fail is already established from the measurement above. What isn't yet established is which bit does it. Starting from &lt;code&gt;600&lt;/code&gt; and adding one bit at a time — read bits (&lt;code&gt;040&lt;/code&gt;, &lt;code&gt;004&lt;/code&gt;) kept separate from write bits (&lt;code&gt;020&lt;/code&gt;, &lt;code&gt;002&lt;/code&gt;) — settles that:&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;cat&lt;/span&gt; /tmp/ssh-demo/boundary.sh
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-demo
&lt;span class="k"&gt;for &lt;/span&gt;m &lt;span class="k"&gt;in &lt;/span&gt;600 604 620 640 660 662 664 666&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/boundary_inner.conf"&lt;/span&gt;
  /usr/bin/ssh &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/boundary_outer.conf"&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; myhost &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"mode=&lt;/span&gt;&lt;span class="nv"&gt;$m&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="k"&gt;done&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;bash /tmp/ssh-demo/boundary.sh
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;604 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;620 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;640 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;660 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;662 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;664 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;666 &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;boundary_outer.conf&lt;/code&gt; stays fixed at &lt;code&gt;600&lt;/code&gt; and contains a single line, &lt;code&gt;Include /tmp/ssh-demo/boundary_inner.conf&lt;/code&gt;. Only &lt;code&gt;boundary_inner.conf&lt;/code&gt;'s own permissions moved:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;owner&lt;/th&gt;
&lt;th&gt;group&lt;/th&gt;
&lt;th&gt;other&lt;/th&gt;
&lt;th&gt;group write&lt;/th&gt;
&lt;th&gt;other write&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;&lt;code&gt;600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;604&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;r--&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;620&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;-w-&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;640&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;r--&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;exit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;660&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;662&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;-w-&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;664&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;r--&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;666&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;rw-&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Whether a read bit is set (&lt;code&gt;604&lt;/code&gt;, &lt;code&gt;640&lt;/code&gt;) makes no difference to the result. One write bit on group or other, and it fails (&lt;code&gt;620&lt;/code&gt; onward) — matching &lt;code&gt;readconf.c&lt;/code&gt;'s condition, &lt;code&gt;(sb.st_mode &amp;amp; 022) != 0&lt;/code&gt;, exactly. &lt;code&gt;022&lt;/code&gt; in octal is group-write (&lt;code&gt;020&lt;/code&gt;) OR'd with other-write (&lt;code&gt;002&lt;/code&gt;); the read bits (&lt;code&gt;044&lt;/code&gt;) never enter into it.&lt;/p&gt;

&lt;p&gt;The earlier &lt;code&gt;Include&lt;/code&gt; measurement — outer file pinned at &lt;code&gt;600&lt;/code&gt;, only the inner file's permissions varied — already satisfied this condition. A &lt;code&gt;600&lt;/code&gt; parent doesn't shield an included file sitting at &lt;code&gt;664&lt;/code&gt; or &lt;code&gt;666&lt;/code&gt;. The parent's own permissions play no role in the check applied to whatever it &lt;code&gt;Include&lt;/code&gt;s.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Bad owner or permissions&lt;/code&gt;: One Message for Two Different Causes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Bad owner or permissions&lt;/code&gt; comes out of a single &lt;code&gt;fatal()&lt;/code&gt; call in &lt;code&gt;readconf.c&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_uid&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_uid&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;getuid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mode&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mo"&gt;022&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bad owner or permissions on %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two conditions joined by &lt;code&gt;||&lt;/code&gt; produce the same one-line message regardless of which one fired. A file owned by neither root nor the current user (&lt;code&gt;st_uid&lt;/code&gt; mismatch) and a file with a group- or other-write bit set (&lt;code&gt;st_mode &amp;amp; 022&lt;/code&gt;) are indistinguishable from the message alone. Telling them apart takes a separate &lt;code&gt;stat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%u %Lp'&lt;/span&gt; /tmp/ssh-demo/boundary_inner.conf
503 664
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;503&lt;/code&gt; matches this account's own &lt;code&gt;id -u&lt;/code&gt;, so ownership checks out — the failure here traces to permissions (&lt;code&gt;664&lt;/code&gt;'s group-write bit), not to a mismatched owner. The owner-mismatch branch — a file that ends up owned by someone else after being copied over shared storage, say — wasn't exercised here, since setting up a second user account was out of reach; that side stays at what the upstream source's condition says, not something measured directly.&lt;/p&gt;

&lt;p&gt;The path in the message deserves attention too. &lt;code&gt;filename&lt;/code&gt; names whichever specific file was open at the time. In the &lt;code&gt;Include&lt;/code&gt; measurement above, the parent &lt;code&gt;outer.conf&lt;/code&gt; passes the check at &lt;code&gt;600&lt;/code&gt;, so the error only ever names the included &lt;code&gt;inner.conf&lt;/code&gt;. Once a config is split across files, check which file's path the error actually names before touching anything — fixing the parent's permissions does nothing if the real cause sits in the included file.&lt;/p&gt;

&lt;p&gt;One path that actually lands on this boundary, confirmed here: with the shell's &lt;code&gt;umask&lt;/code&gt; set to &lt;code&gt;002&lt;/code&gt;, a new file created by redirection comes out at &lt;code&gt;664&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;umask
&lt;/span&gt;022
&lt;span class="nv"&gt;$ &lt;/span&gt;bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"umask 002; printf 'Host x&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    Port 22&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;' &amp;gt; /tmp/ssh-demo/umask_test_config"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%Lp'&lt;/span&gt; /tmp/ssh-demo/umask_test_config
664
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This environment's default &lt;code&gt;umask&lt;/code&gt; is &lt;code&gt;022&lt;/code&gt;, which tops out at &lt;code&gt;644&lt;/code&gt; on its own — &lt;code&gt;002&lt;/code&gt; is a value someone sets deliberately for group sharing, not something a default-umask environment ever produces by itself. &lt;code&gt;cp&lt;/code&gt; behaves differently:&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;bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"umask 002; cp /tmp/ssh-demo/src_600.conf /tmp/ssh-demo/umask_test_cp600.conf"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%Lp'&lt;/span&gt; /tmp/ssh-demo/umask_test_cp600.conf
600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cp&lt;/code&gt; on this machine duplicates the source file's own permissions and doesn't go through &lt;code&gt;umask&lt;/code&gt; at all. What trips the boundary isn't copying a config into place — it's generating one fresh: a shell redirect, an editor save, a template rendering. Running the &lt;code&gt;umask&lt;/code&gt;-created file through &lt;code&gt;Include&lt;/code&gt; fails for the same reason as the boundary sweep above:&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;/usr/bin/ssh &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; /tmp/ssh-demo/umask_outer.conf &lt;span class="nt"&gt;-G&lt;/span&gt; x &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check 4 — &lt;code&gt;Match host&lt;/code&gt; vs &lt;code&gt;originalhost&lt;/code&gt;: Removing &lt;code&gt;Port 2200&lt;/code&gt; to See Which One Fired
&lt;/h2&gt;

&lt;p&gt;The last check: delete the value you suspect matters, and check whether the result flips the way the theory predicts — rather than taking one matching run as confirmation.&lt;/p&gt;

&lt;p&gt;Back to the contradiction Check 1 left open. What settles it is in the man page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The criteria for the &lt;code&gt;host&lt;/code&gt; keyword are matched against the target hostname, after any substitution by the &lt;code&gt;Hostname&lt;/code&gt; or &lt;code&gt;CanonicalizeHostname&lt;/code&gt; options. The &lt;code&gt;originalhost&lt;/code&gt; keyword matches against the hostname as it was specified on the command-line.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://man.openbsd.org/ssh_config" rel="noopener noreferrer"&gt;&lt;code&gt;ssh_config(5)&lt;/code&gt;, OpenBSD manual pages&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;host&lt;/code&gt; is compared &lt;em&gt;after&lt;/em&gt; &lt;code&gt;HostName&lt;/code&gt; substitution. The config sets &lt;code&gt;HostName 127.0.0.1&lt;/code&gt;, so &lt;code&gt;Match host myhost&lt;/code&gt; compares &lt;code&gt;myhost&lt;/code&gt; against &lt;code&gt;127.0.0.1&lt;/code&gt; and never matches. &lt;code&gt;originalhost&lt;/code&gt; compares against what was literally typed on the command line, so &lt;code&gt;Match originalhost myhost&lt;/code&gt; does match — but matching alone still doesn't hand &lt;code&gt;Port&lt;/code&gt; to &lt;code&gt;2300&lt;/code&gt; while &lt;code&gt;Host&lt;/code&gt;'s &lt;code&gt;Port 2200&lt;/code&gt; is set first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host myhost
    HostName 127.0.0.1
    User specific-user
    Port 2200          # present in the first run, removed in the second

Match originalhost myhost
    Port 2300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_originalhost_only &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(user|port) '&lt;/span&gt;
user specific-user
port 2200

&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_fixed &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(user|port) '&lt;/span&gt;
user specific-user
port 2300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting &lt;code&gt;Port 2200&lt;/code&gt; flips the result exactly as predicted. That's still one data point, though, on a config where &lt;code&gt;Match&lt;/code&gt; was already confirmed to fire.&lt;/p&gt;

&lt;p&gt;The actual ablation test applies the same move to the config that started this section — &lt;code&gt;Match host myhost user specific-user&lt;/code&gt;, the one where &lt;code&gt;Match&lt;/code&gt; was never established to fire at all. Remove &lt;code&gt;Host&lt;/code&gt;'s &lt;code&gt;Port 2200&lt;/code&gt;, so first-value-wins has nothing left to defend, and leave &lt;code&gt;Match&lt;/code&gt;'s &lt;code&gt;Port 2300&lt;/code&gt; in place. If &lt;code&gt;Match&lt;/code&gt; had ever fired here, this is exactly the config where &lt;code&gt;2300&lt;/code&gt; should now win by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host myhost
    HostName 127.0.0.1
    User specific-user

Match host myhost user specific-user
    Port 2300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_noport &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^port '&lt;/span&gt;
port 22

&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_noport &lt;span class="nt"&gt;-G&lt;/span&gt; specific-user@myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^port '&lt;/span&gt;
port 22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both calls land on &lt;code&gt;22&lt;/code&gt;, SSH's compiled-in default — not &lt;code&gt;2200&lt;/code&gt; (nothing left for first-value-wins to defend), and not &lt;code&gt;2300&lt;/code&gt; either. &lt;code&gt;Match host myhost&lt;/code&gt; never fired, under either way of calling the host, exactly as the man page's substitution rule predicts. Neither of Check 1's explanations was actually the reason: there was no &lt;code&gt;Port&lt;/code&gt; conflict to resolve, because &lt;code&gt;Match&lt;/code&gt; never matched in the first place.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;grep&lt;/code&gt; detail worth flagging:&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;/usr/bin/ssh &lt;span class="nt"&gt;-F&lt;/span&gt; config_match_noport &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep &lt;/span&gt;port
port 22
gatewayports no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;grep port&lt;/code&gt; without an anchor also catches &lt;code&gt;gatewayports no&lt;/code&gt; — every command above uses &lt;code&gt;grep '^port '&lt;/code&gt; for exactly this reason.&lt;/p&gt;

&lt;p&gt;To pin down when &lt;code&gt;Match host&lt;/code&gt; does and doesn't fire, and separate that cleanly from the &lt;code&gt;Port&lt;/code&gt; conflict, run all four combinations of condition keyword × &lt;code&gt;Port 2200&lt;/code&gt; present/absent, holding everything else fixed — same &lt;code&gt;user specific-user&lt;/code&gt; clause, same &lt;code&gt;-G myhost&lt;/code&gt; call in all four cells:&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;cat &lt;/span&gt;cell.sh
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-demo
&lt;span class="k"&gt;for &lt;/span&gt;cond &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"host myhost user specific-user"&lt;/span&gt; &lt;span class="s2"&gt;"originalhost myhost user specific-user"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  for &lt;/span&gt;port &lt;span class="k"&gt;in &lt;/span&gt;2200 none&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Host myhost\n    HostName 127.0.0.1\n    User specific-user\n'&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; none &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'    Port %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\nMatch %s\n    Port 2300\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cond&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/cell.conf"&lt;/span&gt;
    &lt;span class="nb"&gt;chmod &lt;/span&gt;600 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/cell.conf"&lt;/span&gt;
    &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/usr/bin/ssh &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/cell.conf"&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; myhost | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^port '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Match %-44s Port=%-4s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cond&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;done
done&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;bash cell.sh
Match host myhost user specific-user               &lt;span class="nv"&gt;Port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2200 port 2200
Match host myhost user specific-user               &lt;span class="nv"&gt;Port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none port 22
Match originalhost myhost user specific-user       &lt;span class="nv"&gt;Port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2200 port 2200
Match originalhost myhost user specific-user       &lt;span class="nv"&gt;Port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none port 2300
&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;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Port 2200&lt;/code&gt; present&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Port 2200&lt;/code&gt; removed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Match host ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;port 2200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;port 22&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Match originalhost ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;port 2200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;port 2300&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;Port 2200&lt;/code&gt; in place, both conditions read &lt;code&gt;port 2200&lt;/code&gt; — from this row alone, there's no telling whether &lt;code&gt;Match&lt;/code&gt; failed to fire or fired and lost to first-value-wins. Removing it splits the two apart: &lt;code&gt;originalhost&lt;/code&gt; moves to &lt;code&gt;2300&lt;/code&gt; (&lt;code&gt;Match&lt;/code&gt; fired), &lt;code&gt;host&lt;/code&gt; falls to the compiled-in default &lt;code&gt;22&lt;/code&gt; (&lt;code&gt;Match&lt;/code&gt; never fired at all).&lt;/p&gt;

&lt;p&gt;Deleting a value, flipping a flag, dropping a cache layer, removing a dependency — same move each time, and it's what turns "a setting that looks like it's taking effect" into either confirmed or exposed as dead weight. This check earns its place last on the list because a single passing run can't show the value you're testing caused the result; deleting it and watching the comparison move is the closest thing on this list to a controlled experiment.&lt;/p&gt;

&lt;p&gt;Check 1 and Check 4 are both things a reviewer can redo just by rerunning the same commands after a draft lands. Check 2 and Check 3 aren't — they require deciding whether the enforcing code plausibly exists and which path reaches it, which means opening the source before deciding what to even measure. The "survives a re-run" failure from the opening is exactly the case where the last two are necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What These Four Checks Don't Cover — Apple's &lt;code&gt;ssh&lt;/code&gt; Binary and the Upstream Source
&lt;/h2&gt;

&lt;p&gt;These four worked on the two failures above; they aren't a complete system for catching path-of-measurement errors, and this piece leaves gaps of its own.&lt;/p&gt;

&lt;p&gt;First, target identity was never confirmed. What ran here is Apple's build of &lt;code&gt;ssh&lt;/code&gt;; what got read was the upstream source. There's no proof they're the same build — which is exactly why the default-path claim above stays a prediction rather than a measurement. But "does the binary I ran correspond to the source I'm reading" is a question that should get settled before reasoning about paths at all, not folded into a single disclaimer.&lt;/p&gt;

&lt;p&gt;Second, the expected result isn't independently grounded. The "&lt;code&gt;664&lt;/code&gt; should fail" prediction is derived from reading the same source used to test it. A misread source and a matching misread test would agree with each other and look confirmed.&lt;/p&gt;

&lt;p&gt;Third, there's no direct observation of the path itself. &lt;code&gt;Include&lt;/code&gt; failing is evidence the check ran, but nothing here traced or logged the actual branch taken. How far &lt;code&gt;ssh -G&lt;/code&gt;'s resolved config represents real-connection behavior also wasn't checked in this piece.&lt;/p&gt;

&lt;p&gt;And structurally: Check 1 isn't a measurement at all — it only works when a draft happens to state two competing explanations in the same place; a consistently wrong draft sails through it. Check 3 and Check 4 are really the same underlying move (change an input, watch the causal effect) rather than two clean categories. Read this as four heuristics ordered cheapest-first, not a taxonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reviewer's Own Accuracy — 10 of 36 Adversarial Review Findings Were Later Overturned
&lt;/h2&gt;

&lt;p&gt;None of this makes the checking process infallible. In a separate pre-publication audit of a different draft, the same style of adversarial, disprove-it-first review flagged 36 issues. Of those, 10 were later overturned — the finding itself judged wrong or overreaching, not the underlying draft. That's an overturn rate of just under 3 in 10, and it's the only honest name for it — "error rate" would claim more than is warranted, because whether those 10 overturn decisions were themselves correct hasn't been independently checked. What the number actually describes is that the second-pass review produced a 10/36 overturn outcome; nothing stronger than that.&lt;/p&gt;

&lt;p&gt;Running more automated checks isn't the fix by itself, either. What did the work here wasn't the count of checks — it was two of the four refusing to accept a negative claim without evidence: Check 2 asking whether the enforcing code exists, Check 3 insisting on a case that has to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Questions for Checking a Measurement You've Been Handed
&lt;/h2&gt;

&lt;p&gt;The four checks above are concrete moves against one small system. The questions below are the level above them — what each check is actually standing in for. When a write-up lands, these are worth asking in order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Am I measuring the same target the source describes?&lt;/strong&gt; Does the binary that ran correspond to the source or docs being treated as ground truth?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the expected result independently grounded?&lt;/strong&gt; Is the "this should fail" prediction riding on the same misreading as the thing it's supposed to test?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is there evidence the intended path actually fired&lt;/strong&gt; — or is a nearby path succeeding being read as evidence for the target path?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the result causally depend on the variable that changed?&lt;/strong&gt; Delete the value, flip the condition, and check whether the result actually moves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Everything passed" or "every variant gave the same result" isn't evidence of success on its own — read it as a prompt to check question 3 before crediting the thing under test. If four differently-labeled cases all come back identical, the first thing worth doubting isn't the target's robustness. It's whether those four cases ever took four different paths.&lt;/p&gt;

&lt;p&gt;"I verified it" guarantees a command ran and the output is genuine. It does not guarantee the path that produced that output passed through the mechanism the claim is actually about. And unlike a faked output, this kind of failure survives a re-run — the same input in the same environment comes back identical no matter how many times it's replayed. What's left for whoever's on the receiving end isn't re-reading the output. It's reconstructing the path itself and checking it independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/openssh/openssh-portable/blob/V_10_2_P1/ssh_config.5" rel="noopener noreferrer"&gt;&lt;code&gt;ssh_config(5)&lt;/code&gt;, &lt;code&gt;openssh-portable&lt;/code&gt; at &lt;code&gt;V_10_2_P1&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openssh/openssh-portable/blob/V_10_2_P1/ssh.c" rel="noopener noreferrer"&gt;&lt;code&gt;ssh.c&lt;/code&gt;, &lt;code&gt;openssh-portable&lt;/code&gt; at &lt;code&gt;V_10_2_P1&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openssh/openssh-portable/blob/V_10_2_P1/readconf.c" rel="noopener noreferrer"&gt;&lt;code&gt;readconf.c&lt;/code&gt;, &lt;code&gt;openssh-portable&lt;/code&gt; at &lt;code&gt;V_10_2_P1&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config" rel="noopener noreferrer"&gt;&lt;code&gt;ssh_config(5)&lt;/code&gt;, OpenBSD manual pages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ssh</category>
      <category>linux</category>
      <category>testing</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Building a 3D Scene from 30 Photos: Getting Gaussian Splatting Running on Colab</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:06:32 +0000</pubDate>
      <link>https://dev.to/nomurasan/building-a-3d-scene-from-30-photos-getting-gaussian-splatting-running-on-colab-4n5p</link>
      <guid>https://dev.to/nomurasan/building-a-3d-scene-from-30-photos-getting-gaussian-splatting-running-on-colab-4n5p</guid>
      <description>&lt;p&gt;I wanted to touch Gaussian Splatting (3DGS), but every time I tried to start with the paper I got stuck at the math. So I tried running it on Colab first. Once "what goes in, what comes out" was visible on my own screen, the paper was much easier to come back to. This is the record of doing it in that order.&lt;/p&gt;

&lt;p&gt;The deeper theory is already covered better by other writers. Here I am only writing about "what runs, in the first 30 minutes."&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Yes, a free Google Colab GPU runtime (T4) is enough to train a Gaussian Splatting scene end to end. In this walkthrough, ~5 minutes to download the dataset, 15-30 minutes to train 7,000 iterations, and ~5 minutes to render test views — no prior 3D graphics or NeRF background needed. The walkthrough itself trains on the NeRF Synthetic dataset (which already ships with per-image camera pose data), not on photos you shoot yourself. For a real-world scene, the pipeline needs 30-300 photos plus camera pose data that you generate by running COLMAP first — that step is not covered here. The output is an exportable &lt;code&gt;.ply&lt;/code&gt; Gaussian point cloud you can rotate in a browser with &lt;a href="https://superspl.at/editor" rel="noopener noreferrer"&gt;SuperSplat&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&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;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;Google Colab (GPU runtime: T4 worked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training data&lt;/td&gt;
&lt;td&gt;NeRF Synthetic dataset (the one the official repo recommends)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;~5 min data download + 15-30 min training + ~5 min rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You do not need prior knowledge of 3D graphics or NeRF. If you can read &lt;code&gt;pip install&lt;/code&gt;, you can follow along. I'll keep the gotchas at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Gaussian Splatting actually doing
&lt;/h2&gt;

&lt;p&gt;Roughly: "Given a set of photos, place millions of transparent ellipsoids (Gaussians) in space such that, from any viewing angle, the rendered view matches the photo of that angle."&lt;/p&gt;

&lt;p&gt;Unlike textbook 3D models (collections of triangle polygons), Gaussian Splatting represents the scene as a swarm of points, each with position, color, opacity, and shape. Things that are hard to express with polygons — fur, smoke — come out reasonably natural.&lt;/p&gt;

&lt;p&gt;Compared with NeRF (Neural Radiance Fields), the difference is sharper:&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;NeRF&lt;/th&gt;
&lt;th&gt;Gaussian Splatting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scene representation&lt;/td&gt;
&lt;td&gt;Neural network weights (black box)&lt;/td&gt;
&lt;td&gt;Explicit 3D Gaussian point cloud (exportable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render speed&lt;/td&gt;
&lt;td&gt;Slow (seconds per frame)&lt;/td&gt;
&lt;td&gt;Fast (real-time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Train speed&lt;/td&gt;
&lt;td&gt;Slow (hours)&lt;/td&gt;
&lt;td&gt;Fast (tens of minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility with downstream tools&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Easy to drop into Blender etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If NeRF is "an AI that memorized the scene," Gaussian Splatting is "a point cloud file of the scene." The fact that the output comes out as a file you can hand to other tools later was, personally, the bigger differentiator than the speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data flow, before touching code
&lt;/h2&gt;

&lt;p&gt;Before any code, mapping "what goes in / what comes out":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Input]
  ├── Image files (PNG/JPG)               30-300 per scene
  ├── Camera info (transforms_train.json) Where each image was shot from
  └── Initial point cloud (points3D.ply)  Present in COLMAP datasets; absent
                                          in NeRF Synthetic — random init works

  ↓ Training (iterative optimization)

[Intermediate]
  Gaussian point cloud: hundreds of thousands to millions
    Per point: position (x,y,z), color (RGB), opacity, shape (covariance)

  ↓ Render

[Output]
  2D images from arbitrary viewpoints (real-time)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing to notice early: &lt;strong&gt;training does not start without the camera-info JSON&lt;/strong&gt;. "Just a folder of photos" is not enough — you need per-image camera pose data.&lt;/p&gt;

&lt;p&gt;If you want to start from real-world photos, you'd run COLMAP (a Structure-from-Motion tool) first to produce that JSON. Today I'm using the NeRF Synthetic dataset, which ships with the JSON already attached. For a first run, that's the easier path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it on Colab: the copy-paste checklist
&lt;/h2&gt;

&lt;p&gt;The official repo (graphdeco-inria/gaussian-splatting) README is thorough but written for serious users. Getting it to work in Colab in one pass needs a bit of decoding. When I tried it straight, the first data-download URL on the README was already broken, and the Google Drive mirror was rate-limited. Below are the cells that finally got me through, in the order I ran them. Open a fresh Colab notebook, set the runtime to &lt;strong&gt;GPU (T4)&lt;/strong&gt;, and run from the top.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Confirm the GPU is actually attached
&lt;/h3&gt;

&lt;p&gt;If no GPU name shows up, fix the runtime type and reconnect before going further. Every later step assumes a GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;nvidia&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;smi&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Clone the repo (recursively)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;diff-gaussian-rasterization&lt;/code&gt; and &lt;code&gt;simple-knn&lt;/code&gt; are submodules. Forget &lt;code&gt;--recursive&lt;/code&gt; and you'll hit "directory is empty" errors later in the build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;recursive&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;graphdeco&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;inria&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gaussian&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;splatting&lt;/span&gt;
&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gaussian&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;splatting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Build the CUDA extensions
&lt;/h3&gt;

&lt;p&gt;This is the most failure-prone step. Print both the Colab-side CUDA and the PyTorch-side CUDA before installing — if they don't match, the build will silently produce broken binaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;nvcc&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;torch CUDA:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;submodules&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gaussian&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rasterization&lt;/span&gt;
&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;submodules&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;simple&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;
&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;plyfile&lt;/span&gt; &lt;span class="n"&gt;tqdm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Get the dataset
&lt;/h3&gt;

&lt;p&gt;The official tutorial's &lt;code&gt;cseweb.ucsd.edu&lt;/code&gt; &lt;code&gt;nerf_synthetic.zip&lt;/code&gt; URL redirected to 404 when I tried it. The Google Drive mirror gets blocked by &lt;code&gt;gdown&lt;/code&gt; access limits a lot. What worked was pulling just the &lt;code&gt;lego&lt;/code&gt; scene from a HuggingFace mirror that has the dataset extracted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snapshot_download&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pablovela5620/nerf-synthetic-mirror&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;repo_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allow_patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lego/**&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;local_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/content/data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lego&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Train
&lt;/h3&gt;

&lt;p&gt;On my T4 (15 GB) run, 7,000 iterations trained without hitting memory limits. NeRF Synthetic has a white background so I pass &lt;code&gt;--white_background&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lego&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;white_background&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Render
&lt;/h3&gt;

&lt;p&gt;Outputs land in &lt;code&gt;output/&amp;lt;random runid&amp;gt;/&lt;/code&gt;. To avoid hunting for the runid manually, grab the latest run programmatically and render from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;RUN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/content/gaussian-splatting/output/*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getmtime&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendered images appear in &lt;code&gt;output/&amp;lt;runid&amp;gt;/test/ours_7000/renders/&lt;/code&gt;. If the lego excavator's shape reconstructs across all the test viewpoints, training worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas (things that genuinely stopped me)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CUDA build fails
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;diff-gaussian-rasterization&lt;/code&gt; won't build, it's almost always that the Colab CUDA and the PyTorch CUDA are mismatched. &lt;code&gt;!nvcc --version&lt;/code&gt; vs &lt;code&gt;torch.version.cuda&lt;/code&gt; — print both and you'll see the gap. Reinstall PyTorch against the right CUDA and the build goes through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confusion about the initial point cloud (&lt;code&gt;points3D.ply&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;I read the COLMAP docs and convinced myself that "every scene needs &lt;code&gt;sparse/0/points3D.ply&lt;/code&gt;." Actually that's only for real-world photos run through COLMAP. &lt;strong&gt;NeRF Synthetic only needs &lt;code&gt;transforms_*.json&lt;/code&gt; and &lt;code&gt;train.py&lt;/code&gt; starts from a random initial point cloud.&lt;/strong&gt; Once I separated "COLMAP-derived datasets need points3D" from "synthetic datasets don't," the confusion cleared.&lt;/p&gt;

&lt;h3&gt;
  
  
  CUDA out of memory
&lt;/h3&gt;

&lt;p&gt;As Gaussian count grows, VRAM evaporates. In my case, 7,000 iterations on a free-tier T4 (15 GB) ran without issue; pushing toward 30,000 is where I'd expect to hit the ceiling, based on how VRAM use trended as the count grew (I didn't push that far myself). Start at 7,000, confirm it runs, then push iterations up and watch &lt;code&gt;nvidia-smi&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data URLs are unreliable
&lt;/h3&gt;

&lt;p&gt;Saying this twice because it tripped me up most: the official &lt;code&gt;wget&lt;/code&gt; URL redirected to 404, the Google Drive mirror was rate-limited. HuggingFace mirror worked for me, but distribution endpoints come and go. &lt;strong&gt;Before anything else, just confirm the dataset actually downloads&lt;/strong&gt; — that's the most time-saving check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewing the output
&lt;/h2&gt;

&lt;p&gt;After training, the final Gaussian point cloud is at &lt;code&gt;output/&amp;lt;runid&amp;gt;/point_cloud/iteration_&amp;lt;N&amp;gt;/point_cloud.ply&lt;/code&gt;. Open it with one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://superspl.at/editor" rel="noopener noreferrer"&gt;SuperSplat&lt;/a&gt;: browser-based &lt;code&gt;.ply&lt;/code&gt; viewer. Drag and drop, lowest barrier to entry.&lt;/li&gt;
&lt;li&gt;Blender + Gaussian Splatting add-on: useful when composing with other 3D scenes downstream.&lt;/li&gt;
&lt;li&gt;The SIBR viewer bundled with the official repo: requires local Linux/Windows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a first viewing, SuperSplat is the easiest. Throw the &lt;code&gt;.ply&lt;/code&gt; at the browser, rotate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  After running it once
&lt;/h2&gt;

&lt;p&gt;Before running, I had Gaussian Splatting in my head as "some heavy 3D generation thing." After running it once on Colab, the actual flow — "feed in photos and camera info → iterative optimization runs → point cloud file comes out" — turned out to be not that complicated. Data prep is more work than the model is.&lt;/p&gt;

&lt;p&gt;When the 7,000-iteration render came back with the same lego excavator reconstructing from every test angle, that was the moment "ah, this is actually a 3D scene" clicked.&lt;/p&gt;

&lt;p&gt;Trying to read the paper first and stalling on the math, vs. running it first and then reading — for me, running first made the paper readable. The math equations stopped being opaque; they were now "ah, this parameter, this calculation." Some people find it uncomfortable to skip the theory first. I'd say: this order is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to push next
&lt;/h2&gt;

&lt;p&gt;After the first run goes through, three directions look interesting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run on your own data&lt;/strong&gt; — your phone shoots a video, COLMAP produces &lt;code&gt;transforms.json&lt;/code&gt;, train on that. Adds the COLMAP step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move off Colab&lt;/strong&gt; — Colab runtimes die at 12 hours. For serious training, &lt;a href="https://modal.com/" rel="noopener noreferrer"&gt;Modal&lt;/a&gt; charges by the second and lets long runs survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read for the parameters&lt;/strong&gt; — read &lt;a href="https://qiita.com/scomup/items/d5790da25a846e645de1" rel="noopener noreferrer"&gt;scomup's Japanese explainer&lt;/a&gt; while you adjust learning rates and Gaussian split thresholds. Reading while running settles things in.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Does a free Colab GPU actually work for this?
&lt;/h3&gt;

&lt;p&gt;Yes. The T4 GPU on Colab's free tier trained 7,000 iterations without hitting memory limits in this walkthrough. Pushing toward 30,000 iterations is where I'd expect to hit the VRAM ceiling on a 15 GB T4, based on how usage trended as the Gaussian count grew — I didn't personally push that far.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many photos do I need?
&lt;/h3&gt;

&lt;p&gt;For a real-world scene run through COLMAP, the pipeline typically needs 30-300 photos plus per-image camera pose data that COLMAP generates from them. This walkthrough sidesteps that step by training on the NeRF Synthetic dataset, which already ships with the camera pose JSON attached, so no photos need to be shot for a first run.&lt;/p&gt;

&lt;h3&gt;
  
  
  What GPU/CUDA setup do I need to watch for?
&lt;/h3&gt;

&lt;p&gt;Set the Colab runtime to GPU (T4) before starting. The most failure-prone step is building the CUDA extensions (&lt;code&gt;diff-gaussian-rasterization&lt;/code&gt;, &lt;code&gt;simple-knn&lt;/code&gt;): if the Colab-side CUDA version (&lt;code&gt;nvcc --version&lt;/code&gt;) and the PyTorch-side CUDA version (&lt;code&gt;torch.version.cuda&lt;/code&gt;) don't match, the build can silently produce broken binaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does the whole run take?
&lt;/h3&gt;

&lt;p&gt;Roughly 30-40 minutes total: ~5 minutes to download the dataset, 15-30 minutes to train 7,000 iterations, and ~5 minutes to render the test views.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need 3D graphics or NeRF background knowledge first?
&lt;/h3&gt;

&lt;p&gt;No. If you can read &lt;code&gt;pip install&lt;/code&gt;, you can follow the steps. The theory becomes easier to absorb after you've seen the input/output on your own screen, not before.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I view the trained scene afterward?
&lt;/h3&gt;

&lt;p&gt;The final Gaussian point cloud lands at &lt;code&gt;output/&amp;lt;runid&amp;gt;/point_cloud/iteration_&amp;lt;N&amp;gt;/point_cloud.ply&lt;/code&gt;. &lt;a href="https://superspl.at/editor" rel="noopener noreferrer"&gt;SuperSplat&lt;/a&gt; is the lowest-friction option — a browser-based viewer where you drag and drop the &lt;code&gt;.ply&lt;/code&gt; file. Blender with a Gaussian Splatting add-on and the SIBR viewer bundled with the official repo (local Linux/Windows only) are the other two options mentioned in the official tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/graphdeco-inria/gaussian-splatting" rel="noopener noreferrer"&gt;graphdeco-inria/gaussian-splatting (official)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.classmethod.jp/articles/3d-gaussian-splatting-on-colab/" rel="noopener noreferrer"&gt;Gaussian Splatting on Colab (classmethod, JP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/scomup/items/d5790da25a846e645de1" rel="noopener noreferrer"&gt;Gaussian Splatting explainer (scomup, JP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://superspl.at/editor" rel="noopener noreferrer"&gt;SuperSplat (.ply viewer)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/datasets/pablovela5620/nerf-synthetic-mirror" rel="noopener noreferrer"&gt;NeRF Synthetic mirror (HuggingFace)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modal.com/docs/examples/hello_world" rel="noopener noreferrer"&gt;Modal Labs docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>computervision</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Review Checklist Before You Import External AI Agent Definitions</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Fri, 31 Jul 2026 23:36:23 +0000</pubDate>
      <link>https://dev.to/nomurasan/a-review-checklist-before-you-import-external-ai-agent-definitions-i6o</link>
      <guid>https://dev.to/nomurasan/a-review-checklist-before-you-import-external-ai-agent-definitions-i6o</guid>
      <description>&lt;p&gt;You found a public collection of AI agent definitions — maybe for Claude Code, maybe for Codex — and one looks like the role you're missing. The fast path: copy the file into your agents directory and try it. That path skips every step that would tell you what the file does before it runs with your permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Treat every external AI agent definition as untrusted input until you've read the whole file, not the README. Check what it claims to read, write, and execute against what your host actually grants it (for Claude Code, &lt;code&gt;settings.json&lt;/code&gt; permissions, not the definition's own text). Adapt with the smallest diff possible, log every non-cosmetic change as a deliberate local decision, and run a short verification pass — structure, references, a permission-scoped trial, refusal behavior, routing — before the file goes near your real agents directory. If you can, hand the adoption decision to someone who didn't pick the candidate, with one job: try to prove it shouldn't be adopted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start when choosing a candidate
&lt;/h2&gt;

&lt;p&gt;"What should I pull from this collection" starts from what's listed on the other side, not from what your environment is missing. A large catalog of candidates and a large set of roles you actually run are different things — usefulness tracks with finding the right one, not how many files you saved.&lt;/p&gt;

&lt;p&gt;"We need code review" isn't a gap statement; it's too wide to check anything against. Compress it to one sentence: input, expected output, what's explicitly forbidden — &lt;em&gt;nothing today reads an unfamiliar codebase without modifying it and reports confirmed facts separately from inference&lt;/em&gt;, for example. Can't compress it that far? Hold off adopting anything for it; that's a sign the gap isn't concrete yet, not a verdict on the collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the whole file before it touches your environment
&lt;/h2&gt;

&lt;p&gt;A README description and a full definition are different documents. Before adopting, read the candidate file in full and confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what kind of input it expects (a repository, public documentation, an open-ended conversation)&lt;/li&gt;
&lt;li&gt;what it's supposed to produce (a report, a proposed diff, or direct file edits)&lt;/li&gt;
&lt;li&gt;every external file, command, and tool it references&lt;/li&gt;
&lt;li&gt;what it assumes about network access and file-write permissions&lt;/li&gt;
&lt;li&gt;any prohibition it states explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A real file from a public, MIT-licensed collection called &lt;code&gt;agency-agents&lt;/code&gt; shows exactly why this matters: its frontmatter carries fields like &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;emoji&lt;/code&gt;, and &lt;code&gt;vibe&lt;/code&gt; that mean nothing to Claude Code/Codex's own subagent loader, which reads &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and a few fields like &lt;code&gt;tools&lt;/code&gt;. You only catch a gap like that by opening the file yourself.&lt;/p&gt;

&lt;p&gt;Referenced paths and commands can be valid upstream and absent in your environment — don't resolve a broken reference by guessing the author's intent; list what has to change before it runs. Don't drop a candidate straight into a directory your tools watch automatically (for Claude Code, &lt;code&gt;~/.claude/agents/&lt;/code&gt;); stage it read-only first. Permissions depend on host configuration (&lt;code&gt;settings.json&lt;/code&gt;) as much as the file's own claims — check both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapt with the smallest diff you can
&lt;/h2&gt;

&lt;p&gt;Resist rewriting on day one. Apply only what's required to run, and keep the original and your local version comparable. Split every change in two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format adjustments&lt;/strong&gt;: name, frontmatter fields, file location, invocation conditions — mechanical, required to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meaning changes&lt;/strong&gt;: removing a prohibition, widening what the role can touch, changing its output type, swapping a referenced mechanism. These are local design decisions, not adjustments — they need an owner and a stated reason.&lt;/p&gt;

&lt;p&gt;Strip the origin at copy time and you lose the ability to confirm what you were licensed to use (checkable against the LICENSE file) and to pull a future update cleanly. A minimal diff stays legible enough to explain later, small enough to know what to remove if the candidate stops earning its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a passing verification
&lt;/h2&gt;

&lt;p&gt;Parsing the file isn't being ready to run it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structural check&lt;/strong&gt; — required YAML frontmatter fields present and well-formed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference check&lt;/strong&gt; — do referenced files, paths, and commands actually exist here?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped trial&lt;/strong&gt; — permissions narrowed, does it produce the expected output for a realistic input?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refusal check&lt;/strong&gt; — does it decline out-of-scope requests and stop cleanly on failure?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing check&lt;/strong&gt; — same request to it and any similar existing role: which one answers?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fifth isn't about picking a winner — it's finding ambiguous routing before a user has to guess. If the boundary is unclear, fix the name or trigger conditions, or don't keep both.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agency-agents&lt;/code&gt; ships its own reference check — a script cross-checking its division list against directories on disk and CI path filters, failing the build on any disagreement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/check-divisions.sh
&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;PASSED: 17 divisions consistent across divisions.json, directories, scripts, and CI.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick whatever's supposed to be the source of truth, and write a check that fails loudly the moment anything disagrees, instead of finding out at runtime. Record what you tested, not just pass/fail — input, observed output, which permissions went unexercised. An unexercised permission isn't a verified one; mark it untested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide update and removal before you adopt
&lt;/h2&gt;

&lt;p&gt;Decide who reviews the next update at adoption time, not after it lands. Automatic tracking needs a pre-change review step and a rollback path; pinning a version needs a way to learn about security fixes without it.&lt;/p&gt;

&lt;p&gt;When an update arrives, read three things separately before applying anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the role you're currently using&lt;/li&gt;
&lt;li&gt;the gap it was adopted to fill&lt;/li&gt;
&lt;li&gt;the local changes layered on top&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't renew out of inertia — confirm the original reason still holds. Removal isn't failure: if an existing role covers the same ground now, terms changed, or nobody owns it, stop referencing it and confirm nothing breaks. Check what still calls it before deleting — a dangling reference is worse than a file left in place. Deferring these decisions converts a reviewed import into an unowned asset with no traceable origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  An adoption decision sheet template
&lt;/h2&gt;

&lt;p&gt;If the questions change every time, only the impression survives to the final decision. Answer the same fields for every candidate; prose is fine if every field is addressed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fit with the gap&lt;/strong&gt; — which part fills it, which part is out of scope?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlap&lt;/strong&gt; — closest existing role, why it doesn't already cover this, condition for running both?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance and terms&lt;/strong&gt; — source, exact version (Git commit or tag), the LICENSE file itself, attribution requirements, host-side permissions needed. Mark unconfirmed items, and decide whether that blocks adoption or only unattended use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptation diff&lt;/strong&gt; — format adjustments and meaning changes, separated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trial results&lt;/strong&gt; — successes, refusals, side effects a failed trial left behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption state&lt;/strong&gt; — adopted, on trial, on hold, declined, or marked for removal. "Confirmed" alone doesn't say what was confirmed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worth less as a one-time filter than as a record: "why did we bring this in" gets a written answer later, not "it seemed useful at the time."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Reviewer has to try to falsify
&lt;/h2&gt;

&lt;p&gt;Whoever sourced the candidate is already inclined to see it favorably. Someone uninvolved in sourcing it should read the sheet and the file and try to disprove the adoption:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the gap actually exist?&lt;/strong&gt; Was the search for an existing equivalent wide enough?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the paper trail hold up?&lt;/strong&gt; License inferred from the README, or confirmed at the source? No traceable origin is a fail condition, not a caveat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What did adaptation actually change?&lt;/strong&gt; A removed prohibition or added permission has to be labeled a local decision, not described as the original definition's behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is there a way back?&lt;/strong&gt; Can the trial's changes be reverted, the reference removed cleanly, the blast radius traced from the sheet alone?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While any of these four stays an open Blocker, the candidate doesn't move to its permanent location. The Reviewer's job isn't polishing a version to paper over the gap — an unsupported claim gets deleted or marked open. Whoever finalizes the adopted version (the Compiler) works only from what's already confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sources of truth: upstream and your local copy
&lt;/h2&gt;

&lt;p&gt;Once adopted, it's easy to lose track of which copy is authoritative for what:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the upstream original&lt;/strong&gt; — authoritative for the design and update history the maintainer publishes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;your local version&lt;/strong&gt; — authoritative for the permissions, prohibitions, placement, and invocation conditions you actually approved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When upstream changes, read why before applying the diff, and check whether it touches what you actually rely on — not updating can be legitimate, not negligence. If you improve your local copy, don't describe that as upstream's behavior; that makes a future bug harder to attribute. If upstream disappears or its terms change, you'll want the version and scope you adopted under preserved — not instead of checking terms while available, but as the fallback once they aren't.&lt;/p&gt;

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

&lt;p&gt;Importing an external AI agent definition without an accident scales down to seven steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the whole candidate file first; check claimed permissions against your host configuration.&lt;/li&gt;
&lt;li&gt;Adapt with the smallest diff, recording format adjustments and meaning changes separately.&lt;/li&gt;
&lt;li&gt;Verify structure, references, a scoped trial, refusal behavior, and routing, before adoption.&lt;/li&gt;
&lt;li&gt;Decide who owns the next update and when you'd remove the role, at adoption time.&lt;/li&gt;
&lt;li&gt;Fill out a decision sheet with the same fields for every candidate.&lt;/li&gt;
&lt;li&gt;Have someone who didn't source the candidate try to falsify the adoption.&lt;/li&gt;
&lt;li&gt;Keep upstream and your local version as two sources of truth, connected through provenance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these steps is expensive alone. What they replace is one unrecorded step — "it looked fine, so I copied it in" — expensive exactly when it turns out to have been wrong.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Does this apply to any AI agent framework, or only Claude Code?
&lt;/h3&gt;

&lt;p&gt;Not Claude Code/Codex-specific — it applies anywhere an external, executable role definition gets adopted into an environment with real permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the definition references a tool or command I don't have installed?
&lt;/h3&gt;

&lt;p&gt;Treat it as a required adaptation. List every file, command, and tool referenced, and confirm each before adoption — a missing reference that only shows up as unexplained behavior at runtime, not a clear error, is exactly what the full-file review catches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run this checklist alone, without a separate Reviewer?
&lt;/h3&gt;

&lt;p&gt;You can review and verify solo. Falsification is harder alone — checking your own decision with intent to disprove it, after you're already attached to the candidate. At minimum, revisit the decision sheet after a delay and read it looking for what would make you say no.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the upstream repository is deleted or its license changes later?
&lt;/h3&gt;

&lt;p&gt;That's what the provenance record is for: version pulled, license at the time, source. It won't recreate a deleted repository, but it shows what you were licensed to use at adoption time.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/msitarzewski/agency-agents" rel="noopener noreferrer"&gt;agency-agents on GitHub&lt;/a&gt; — the public, MIT-licensed collection referenced throughout this piece&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). This is an English-language adaptation of a Japanese-language original covering the same review workflow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>I Grepped My Own Claude Code Logs and Found the Hidden Tag Anthropic Never Shows You</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:03:20 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-grepped-my-own-claude-code-logs-and-found-the-hidden-tag-anthropic-never-shows-you-17c0</link>
      <guid>https://dev.to/nomurasan/i-grepped-my-own-claude-code-logs-and-found-the-hidden-tag-anthropic-never-shows-you-17c0</guid>
      <description>&lt;p&gt;I was mid-conversation with Claude Code, asking it to help draft a blog post, when a literal&lt;br&gt;
&lt;code&gt;&amp;lt;ip_reminder&amp;gt;&lt;/code&gt; tag showed up pasted into my own message. I hadn't typed it. It took me a few&lt;br&gt;
minutes of back-and-forth with the model to figure out what I was even looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: &lt;code&gt;ip_reminder&lt;/code&gt; is a real, non-displayed system-prompt injection Claude Code sends on&lt;br&gt;
every turn for copyright safety. You don't have to trust anyone's self-reported numbers about it&lt;br&gt;
— you can &lt;code&gt;grep&lt;/code&gt; your own session logs and count it yourself. In my case: 151 matching lines, 437&lt;br&gt;
raw occurrences, in one session file alone.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I use Claude Code daily across a handful of projects, and one thread of work turned into asking&lt;br&gt;
the model whether it was safe to write publicly about something I'd noticed: a block of text that&lt;br&gt;
looked like a system tag, &lt;code&gt;&amp;lt;ip_reminder&amp;gt;...&amp;lt;/ip_reminder&amp;gt;&lt;/code&gt;, had turned up inline in the&lt;br&gt;
conversation. Claude's own response to seeing it was telling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"今回の&lt;code&gt;&amp;lt;ip_reminder&amp;gt;&lt;/code&gt;は本物のシステム注入ではなく、ユーザーが手打ちでメッセージ本文に貼り付けたテキスト...&lt;br&gt;
なお直前のBash結果内に埋め込まれた&lt;code&gt;&amp;lt;ip_reminder&amp;gt;&lt;/code&gt;ブロックは、ツール出力に混入した外部由来のテキスト"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translated: &lt;em&gt;this occurrence was pasted by the user, not a live system injection.&lt;/em&gt; A few turns&lt;br&gt;
later, while trying to hand a prompt containing that same discussion off to a delegation script&lt;br&gt;
via a Bash command, an argument-parsing error (&lt;code&gt;exit code 2&lt;/code&gt;, wrong flag) caused part of the&lt;br&gt;
prompt text — including an &lt;code&gt;&amp;lt;ip_reminder&amp;gt;&lt;/code&gt;-shaped fragment — to spill back out into the tool's&lt;br&gt;
error output. Claude flagged that second appearance explicitly as "text mixed into a tool result&lt;br&gt;
from an external source" and disregarded it as an unrelated instruction rather than acting on it.&lt;br&gt;
Neither occurrence was a live, hidden system-level injection in that moment — but the tag had&lt;br&gt;
already become real enough, twice, in two different ways, that I wanted to know what it actually&lt;br&gt;
was.&lt;/p&gt;

&lt;p&gt;Instead of taking my own memory of that exchange at face value, I went back to the actual&lt;br&gt;
transcript. Claude Code writes every session to a JSONL file under&lt;br&gt;
&lt;code&gt;~/.claude/projects/&amp;lt;project-key&amp;gt;/&lt;/code&gt;, one line per event, and nothing in that file is edited after&lt;br&gt;
the fact — it's the closest thing I have to a raw record of what really happened.&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;# project_key = your cwd with slashes replaced by dashes&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;LOG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# how many lines mention the tag at all&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"ip_reminder"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl

&lt;span class="c"&gt;# how many literal opening/closing tags actually appear&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;ip_reminder&amp;gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;/ip_reminder&amp;gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against the session file where this happened, that gave me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;151 lines matched "ip_reminder"
437 total occurrences of the string
17  literal &amp;lt;ip_reminder&amp;gt; opening tags
0   literal &amp;lt;/ip_reminder&amp;gt; closing tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero closing tags against 17 opening tags was the detail that made me stop and actually look&lt;br&gt;
closer, rather than just nod at "yep, it's in there somewhere." Whatever pipeline stage summarizes&lt;br&gt;
or truncates tool output in the transcript apparently drops the closing half of the tag more often&lt;br&gt;
than not — the tag gets cut off, not cleanly removed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The number that changed my mind
&lt;/h2&gt;

&lt;p&gt;The GitHub-Issue side of this story (four Issues on &lt;code&gt;anthropics/claude-code&lt;/code&gt;, all filed by other&lt;br&gt;
people, all independently discovering the same tag) is worth knowing, but I want to be explicit&lt;br&gt;
about which numbers are mine and which are someone else's, because blending them would be&lt;br&gt;
dishonest:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Someone else's numbers (self-reported, not independently verified by Anthropic):&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;Issue&lt;/th&gt;
&lt;th&gt;method&lt;/th&gt;
&lt;th&gt;reported finding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/17601" rel="noopener noreferrer"&gt;#17601&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mitmproxy&lt;/code&gt; traffic capture, 32 days&lt;/td&gt;
&lt;td&gt;10,577 hidden injections; 15.79% direct context-window overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/21214" rel="noopener noreferrer"&gt;#21214&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;local log grep/count&lt;/td&gt;
&lt;td&gt;63,046 injection pairs; ~5,358,910 tokens; ~$133 at Opus 4 API rates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/22955" rel="noopener noreferrer"&gt;#22955&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;inspection of &lt;code&gt;/cost&lt;/code&gt; and &lt;code&gt;/status&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;undisclosed system prompt, ~10–15k tokens; not broken out in cost UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/50516" rel="noopener noreferrer"&gt;#50516&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;bypass/effectiveness testing&lt;/td&gt;
&lt;td&gt;reminder is trivially circumventable; proposed an opt-out setting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All four are &lt;code&gt;state: CLOSED, stateReason: NOT_PLANNED&lt;/code&gt; — confirmed with &lt;code&gt;gh issue view &amp;lt;n&amp;gt; -R&lt;br&gt;
anthropics/claude-code --json state,stateReason&lt;/code&gt; for each. A precursor Issue, #12443 (a different,&lt;br&gt;
malware-warning-flavored hidden reminder, not &lt;code&gt;ip_reminder&lt;/code&gt; itself), is &lt;code&gt;CLOSED, DUPLICATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My own numbers (first-party measurement, same session as above):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I extended the grep into a token-accounting pass, reusing the same JSONL-&lt;code&gt;usage&lt;/code&gt;-field method I'd&lt;br&gt;
used in an earlier post about Claude Code's prompt-cache behavior. I isolated the window of that&lt;br&gt;
session from the first &lt;code&gt;ip_reminder&lt;/code&gt; mention to the end of the file — 432 assistant turns with&lt;br&gt;
usage data — and summed the token fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.claude/projects/&amp;lt;project-key&amp;gt;/&amp;lt;session-id&amp;gt;.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;start_line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_reminder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cache_create&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fresh_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;turns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start_line&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;turns&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;cache_create&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;cache_read&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;fresh_input&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache_create&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cache_read&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fresh_input&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turns=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;turns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cache_create=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cache_create&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cache_read=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cache_read&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fresh_input=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fresh_input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cache_read_share=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cache_read&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&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;turns=432 cache_create=2,251,420 cache_read=148,817,233 fresh_input=1,415
cache_read_share=98.51%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;98.51% of every input-side token in that stretch of conversation was a cache read, not fresh&lt;br&gt;
input. That's not an &lt;code&gt;ip_reminder&lt;/code&gt;-specific number — it's the whole session's cache behavior — but&lt;br&gt;
it puts the Issue #17601 author's 15.79%-of-context-window claim in a useful frame: even a tag&lt;br&gt;
that's a comparatively small slice of a single request compounds when nearly every token in the&lt;br&gt;
conversation is being replayed from cache turn after turn. I have not isolated the injection&lt;br&gt;
block's own token count per turn; that's a harder measurement than counting string occurrences,&lt;br&gt;
and I'm not claiming to have done it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;Call it &lt;strong&gt;grep before you trust the self-report&lt;/strong&gt;: when a vendor's hidden-injection behavior shows&lt;br&gt;
up in your own tool, the fastest way to know if it's actually affecting you is to search your own&lt;br&gt;
logs, not to import someone else's Issue thread as if their numbers were yours.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Someone else's 5,358,910 tokens tells you the phenomenon exists. Your own &lt;code&gt;grep -c&lt;/code&gt; tells you&lt;br&gt;
whether it's happening to you, and how often.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This also reframes what "closed, not_planned" across four Issues actually means for a user. It&lt;br&gt;
doesn't mean the tag stopped existing — Anthropic's own system-prompt documentation&lt;br&gt;
(&lt;code&gt;platform.claude.com/docs/en/release-notes/system-prompts&lt;/code&gt;) still lists &lt;code&gt;ip_reminder&lt;/code&gt; by name&lt;br&gt;
alongside &lt;code&gt;image_reminder&lt;/code&gt;, &lt;code&gt;cyber_warning&lt;/code&gt;, &lt;code&gt;system_warning&lt;/code&gt;, &lt;code&gt;ethics_reminder&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;long_conversation_reminder&lt;/code&gt; (the exact count of listed types varies by model-version doc section&lt;br&gt;
— 5 in one section, 6 in another, when I cross-checked). The name is acknowledged. The content and&lt;br&gt;
cost accounting are not. "Not planned" means the burden of detection stays on you, permanently,&lt;br&gt;
which is exactly why a local, repeatable detection habit is worth more here than a bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Find your project's session log directory:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;ls&lt;/span&gt; ~/.claude/projects/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Older sessions may be gzipped — use &lt;code&gt;zgrep&lt;/code&gt; instead of &lt;code&gt;grep&lt;/code&gt; on &lt;code&gt;*.jsonl.gz&lt;/code&gt; files.)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Count how often &lt;code&gt;ip_reminder&lt;/code&gt; shows up:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"ip_reminder"&lt;/span&gt; ~/.claude/projects/&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;If you want the cache/token breakdown too, adapt the Python script above — swap in your own
session path and it'll print the same four numbers I did.&lt;/li&gt;
&lt;li&gt;If you find it, don't panic and don't assume it's the same volume I saw — sessions differ
wildly in length and topic. The point isn't the absolute number, it's having a repeatable way
to check instead of relying on someone else's screenshot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's the highest &lt;code&gt;ip_reminder&lt;/code&gt; count you find in your own logs — and does it change how you&lt;br&gt;
think about what a "turn" in Claude Code actually costs?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I check my own logs before I quote someone else's numbers. Measured on&lt;br&gt;
Claude Code, macOS, 2026-07-23/24.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This piece started from a Japanese investigative essay I wrote after noticing the same tag in my&lt;br&gt;
own session (published on note.com). This English version is a first-person rewrite built from&lt;br&gt;
my own session-log measurements, not a translation — I worked with AI to shape the prose; the&lt;br&gt;
grep commands, the measurements, and the conclusions are mine.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>claudecode</category>
      <category>anthropic</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Subagent Launch Costs: Audit Before You Try to Reduce</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sun, 26 Jul 2026 23:43:59 +0000</pubDate>
      <link>https://dev.to/nomurasan/why-my-subagent-launches-cost-50k-tokens-before-doing-any-work-4em1</link>
      <guid>https://dev.to/nomurasan/why-my-subagent-launches-cost-50k-tokens-before-doing-any-work-4em1</guid>
      <description>&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Launch your subagent once. Log input + cache_creation + output + finish_reason. Repeat the exact same call. If the second run doesn't reuse cache, the entrance is the bug — not the prompt.&lt;/p&gt;

&lt;p&gt;Only after that baseline exists should you start bounding turns and dollars per profile behind a single wrapper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: I audited a week of &lt;code&gt;sdk-cli&lt;/code&gt; subagent launches. Most launches reused no cache. Several baselines burned tens of thousands of input + cache_creation tokens before the agent had touched a file. I stopped trying to claim a reduction rate from single-shot numbers and made the entrance one wrapper with two profiles, two mandatory stop conditions (&lt;code&gt;--max-turns&lt;/code&gt;, &lt;code&gt;--max-budget-usd&lt;/code&gt;), and a pre-execution ledger — because none of the single-shot deltas I had were controlled A/B measurements.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I was touching subagents and hit a scene I did not expect: a single short request could show tens of thousands of tokens in input + cache_creation. When I audited about a week of launches, most of them showed no cache reuse.&lt;/p&gt;

&lt;p&gt;The first thing you want to do at that point is to publish a reduction rate. Line up numbers from before and after wrapping, subtract, say "N% saved." That reduction rate does not hold. The conditions on either side of the subtraction are different: the model, the size of the request, the tools loaded, the hook state. A subtraction across single-shot numbers is neither the wrapper's effect nor the design's effect.&lt;/p&gt;

&lt;p&gt;So this article is not about reduction. It is about &lt;strong&gt;audit over reduction&lt;/strong&gt; — making the entrance the kind of thing where "why was this one call heavy?" can be answered later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Written in collaboration with AI. Fresh execution logs at the time of the audit were the primary source; official docs were checked for the CLI behavior. Observed facts, interpretation, and unproven claims are kept separate. Final responsibility is mine.&lt;/p&gt;

&lt;p&gt;Author&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A subagent, here, is a worker launched by a main AI to delegate part of the job. Combining several of them lets you split work between roles — investigation and implementation, drafting and review. What I looked at was: for one launch, what gets loaded, where the caps sit, what actually gets recorded.&lt;/p&gt;

&lt;p&gt;Bottom line up front. The wrapper (here, &lt;code&gt;claude-worker.sh&lt;/code&gt;) is a guardrail that pins down profile, turn cap, budget cap, and the sanctioned launch path. The goal is to make one launch's conditions recoverable after the fact, not to save money on that launch. If you want to talk about reduction, you need a separate A/B measurement holding model, request, tools, and hooks fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a short request still costs a lot at launch
&lt;/h2&gt;

&lt;p&gt;A short request still ships a lot of context to be interpreted at launch: system instructions, project rules, hooks, any extra context the entrance loaded. A small body does not imply a small total.&lt;/p&gt;

&lt;p&gt;The awkward part is that the visible reply is short, so the fixed cost is invisible from the user side. "Does this design look OK?" comes back as a few paragraphs and feels like a lightweight consultation. Internally, the worker read a large chunk of input and rebuilt an unreusable cache before writing a single sentence.&lt;/p&gt;

&lt;p&gt;For Claude Code specifically, the &lt;a href="https://code.claude.com/docs/en/context-window" rel="noopener noreferrer"&gt;official context-window docs&lt;/a&gt; note that the system prompt alone is roughly 4,200 tokens, and &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/a&gt;, skill descriptions, and MCP tool names load into context &lt;strong&gt;at the start of every session&lt;/strong&gt;. Thousands to tens of thousands of tokens are already spent before you type your first line.&lt;/p&gt;

&lt;p&gt;Kitchen metaphor: you asked for one cup of tea, and the kitchen was rebuilt for that cup. Halving the tea does very little to the cost of building the kitchen. Trimming a few lines of prompt is not meaningless, but if you skip the fixed launch cost first, you are aiming at the wrong thing.&lt;/p&gt;

&lt;p&gt;There is no single reason cache reuse fails, either. Launch path, loaded config, environment variables, appended context, concurrent runs — all candidates. Because nothing in my week of data changed only one variable at a time, the low reuse rate I saw does not by itself identify the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually need to record to see the launch cost
&lt;/h2&gt;

&lt;p&gt;The prompt text is not enough. To make a per-launch story recoverable you need to tie together, in the same execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;input tokens&lt;/li&gt;
&lt;li&gt;cache_creation tokens&lt;/li&gt;
&lt;li&gt;output tokens&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;tools&lt;/li&gt;
&lt;li&gt;working directory&lt;/li&gt;
&lt;li&gt;hook conditions active during the run&lt;/li&gt;
&lt;li&gt;finish_reason (stop / max_turns / error / auth failure / …)&lt;/li&gt;
&lt;li&gt;whether the request actually completed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current ledger does not go that far — more on that below.&lt;/p&gt;

&lt;p&gt;Splitting consult and impl profiles is a real design choice: they need different tools and different turn budgets. But comparing single-shot numbers across different jobs does not give you the profile's normal cost, and it does not give you a reduction rate.&lt;/p&gt;

&lt;p&gt;Split "cheap" from "cheap-but-finished." A cheaper call that exits asking for missing info costs the fixed startup cost again on the retry.&lt;/p&gt;

&lt;p&gt;Split "cheap" from "cheap-but-authenticated." I once added &lt;code&gt;--bare&lt;/code&gt; to the impl profile to shave startup and got back &lt;code&gt;Not logged in&lt;/code&gt;. Removing &lt;code&gt;--bare&lt;/code&gt; fixed it. If I had judged by token count alone I would have kept it and quietly converted every impl launch into a retry loop.&lt;/p&gt;

&lt;p&gt;The comparison unit is not "the cheapest one call." It is "one call that completed the same request" — same job, same model, same tools, same limits, successes and failures included. Only then does an entrance-level delta make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I banned raw &lt;code&gt;claude -p&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before this change, callers could pick their own flags each time they invoked &lt;code&gt;claude -p&lt;/code&gt;. &lt;code&gt;-p&lt;/code&gt; is print mode — non-interactive, prints output, exits. Great for automation. It also leaves discretion at every call site.&lt;/p&gt;

&lt;p&gt;Discretion is convenient and unauditable. One day a caller sets a turn limit, the next day forgets. Some calls specify a budget, others don't. Some log, some don't. Later, when one launch is 3× everyone else, you cannot tell why &lt;em&gt;this one&lt;/em&gt; was different.&lt;/p&gt;

&lt;p&gt;So I took &lt;code&gt;claude -p&lt;/code&gt; out of the normal path and routed everything through &lt;code&gt;claude-worker.sh&lt;/code&gt;. A wrapper is a thin entrance around the real command that inserts shared configuration and recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the wrapper is shaped — profiles and overrides
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude-worker.sh&lt;/code&gt; reads the profile's defaults and &lt;strong&gt;always&lt;/strong&gt; appends &lt;code&gt;--max-turns&lt;/code&gt; and &lt;code&gt;--max-budget-usd&lt;/code&gt; to the real command. Concrete values live in the profile; I don't publish specific numbers because they should come from your own measurements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude-worker.sh consult &lt;span class="s2"&gt;"check my design concern"&lt;/span&gt;
claude-worker.sh impl &lt;span class="s2"&gt;"edit target and run tests"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skeleton, as pseudocode (design intent, not the implementation):&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;# 1. Resolve profile (unknown name → stop)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;consult|impl|probe&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"unknown profile"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="c"&gt;# 2. Accept only allow-listed overrides (unknown flags do not pass through)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$#)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="nt"&gt;--model&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;--max-turns&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;--cwd&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;--effort&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;--resume&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; ... &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"flag not allowed: &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# 3. Always attach stop conditions (values overridable, existence not)&lt;/span&gt;
&lt;span class="nv"&gt;RESOLVED_MAX_TURNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OPT_MAX_TURNS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$PROFILE_DEFAULT_MAX_TURNS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;RESOLVED_MAX_BUDGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OPT_MAX_BUDGET&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$PROFILE_DEFAULT_MAX_BUDGET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Record pre-execution conditions (no result data)&lt;/span&gt;
append_ledger_pre &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOLVED_MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOLVED_MAX_TURNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT_HEAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FRESH_ENV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 5. exec the real binary (exit code is transparent)&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOLVED_MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOLVED_MAX_TURNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOLVED_MAX_BUDGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values of the two stop conditions can be tuned. Their &lt;strong&gt;existence&lt;/strong&gt; on the command line cannot be removed. "Callers only pick a job type" would be inaccurate — the accurate phrasing is "the profile carries defaults; only allow-listed keys can be overridden."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;consult&lt;/code&gt; and &lt;code&gt;impl&lt;/code&gt; are not moods. They bundle the tools, turn cap, budget cap, and expected output shape for a job type. The caller picks the profile; the wrapper permits overrides only on allow-listed keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guardrail is enforced by the mechanism, not the doc
&lt;/h2&gt;

&lt;p&gt;The important part is that "please always use the wrapper" is not left as a guideline. In automation, when the rule and the physical path disagree, the physical path wins. Old examples get re-read (by another AI even) and the raw call pattern comes right back.&lt;/p&gt;

&lt;p&gt;To make the single path real, raw &lt;code&gt;claude -p&lt;/code&gt; calls stay blocked and the wrapper is registered as the sanctioned channel. This is guardrail work: the shape of the entrance is enforced by the mechanism, not by a document.&lt;/p&gt;

&lt;p&gt;Once the entrance is one place, the place you change is also one place. Cache-reuse settings, a new profile, an added ledger field — you no longer hunt every call site.&lt;/p&gt;

&lt;p&gt;Exit codes are transparent because the wrapper &lt;code&gt;exec&lt;/code&gt;s the real binary. Limit hits and auth failures do not get rewritten into success. Two environment variables mark the entrance so downstream hooks can identify it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE_CODE_ENTRYPOINT=sdk-cli&lt;/code&gt; — observed on headless runs; identifies the CLI/SDK-driven entry, not my wrapper specifically&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE_WORKER=1&lt;/code&gt; — set by the wrapper so hooks can branch on "was this launched by my worker specifically"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding one env var sounds trivial, but it is what lets hooks quiet down &lt;em&gt;only&lt;/em&gt; on the worker path without changing normal interactive behavior. Naming the entrance is not enough — downstream has to be able to identify it too.&lt;/p&gt;

&lt;p&gt;One small warning on passing the prompt: if you rebuild the argv string in shell, quotes and newlines in the prompt can split into separate arguments. The wrapper's job is not to rewrite the prompt but to hand it safely to the real binary. Pass via arg, stdin, or a temp file — pick one and stick with it. Mixing methods per caller ruins your measurement conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you need both &lt;code&gt;--max-turns&lt;/code&gt; and &lt;code&gt;--max-budget-usd&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--max-turns&lt;/code&gt; caps the number of tool-using turns. The &lt;a href="https://code.claude.com/docs/en/cli-reference" rel="noopener noreferrer"&gt;Claude Code CLI reference&lt;/a&gt; documents it as a print-mode (&lt;code&gt;-p&lt;/code&gt;) flag with &lt;strong&gt;no default limit&lt;/strong&gt;. If the cap is hit, the run exits with an error rather than pretending to succeed.&lt;/p&gt;

&lt;p&gt;"No default limit" is not a claim that runs go forever. Normal jobs finish. But if you never say "this kind of consultation may take at most N turns," you have delegated the stop condition entirely to model judgment. When investigation branches or errors chain, a consultation-scoped call can grow to implementation scale before you notice.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--max-budget-usd&lt;/code&gt; is a spend cap on the API calls the execution may make. The reference documents it as another print-mode stop condition. This article deliberately avoids per-model pricing and JPY conversion — plans and contracts vary.&lt;/p&gt;

&lt;p&gt;Turn cap alone is insufficient. If a single turn drags in a large input, few turns can still be heavy. Budget cap alone is insufficient. A run can stay under budget while dragging out too many turns for a "consult." Turn count and total spend are different axes. You need both.&lt;/p&gt;

&lt;p&gt;Not with a single value everywhere. Consult and impl need different completion budgets. A consult cap on impl stops before verification. An impl cap on consult wastes headroom on a job that should have ended short. The caps live on the profile, not as one hard-coded value in the wrapper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[caller] --&amp;gt; B[claude-worker.sh]
    B --&amp;gt; C[resolve profile&amp;lt;br/&amp;gt;consult / impl / probe]
    C --&amp;gt; D[allow-listed overrides&amp;lt;br/&amp;gt;model / cwd / effort ...]
    D --&amp;gt; E["always attach stop conditions&amp;lt;br/&amp;gt;--max-turns / --max-budget-usd"]
    E --&amp;gt; G[[pre-execution ledger&amp;lt;br/&amp;gt;7 fields per row]]
    G --&amp;gt; H(((exec claude -p)))
    H --&amp;gt; I[exit code&amp;lt;br/&amp;gt;transparent to caller]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read the diagram: the stop conditions are added at one entrance instead of being appended by every caller. The ledger (double outline) records only the pre-execution conditions; results (tokens, cost, exit code) do not go here. The separation is discussed in the next section.&lt;/p&gt;

&lt;p&gt;Limits are not a punishment for distrusting the model. They are the boundary both sides agree on for "one call." With the boundary, small jobs stay small and large jobs get treated as impl from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The current ledger records conditions, not usage
&lt;/h2&gt;

&lt;p&gt;The fourth piece is the ledger. &lt;code&gt;~/.claude/logs/claude-worker-ledger.jsonl&lt;/code&gt;, one JSON object per line, appended per launch.&lt;/p&gt;

&lt;p&gt;The current implementation stores only pre-execution fields — seven of them: timestamp, profile, model, max_turns, working directory, first 100 chars of the prompt, and a fresh_env flag. Token counts, cost, exit code, finish_reason, success/failure, response body — none of that is stored. The budget cap is not in the row either.&lt;/p&gt;

&lt;p&gt;So the ledger cannot rank single-shot calls or produce a reduction rate. What it can tell you is: what conditions did this launch try to start with. Whether the execution finished and how much it consumed have to be joined from another source.&lt;/p&gt;

&lt;p&gt;The next improvement is a paired result record. At minimum: execution ID, actual input / cache_creation / output tokens, cost, exit code, finish_reason, and a completion judgment. With those joined on execution ID, "was this run cheap because it succeeded, or cheap because it failed?" becomes answerable from the ledger.&lt;/p&gt;

&lt;p&gt;The goal is not "store everything." Full prompts can contain secrets and PII. Scope the result record to fields that answer "why was this one expensive?" and "did the job complete?" — not raw payloads.&lt;/p&gt;

&lt;p&gt;Monthly totals help with budget forecasting but hide per-run condition differences and failure reasons. A pre-execution-only ledger tells you what conditions were tried but not what happened. Different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the launch cheaper by breaking it
&lt;/h2&gt;

&lt;p&gt;When you are trying to lower startup cost, "just load less at boot" sounds obvious. Sometimes it is right. Not always.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--bare&lt;/code&gt; experiment is the counter-example. On paper, a cheaper launch. In practice, &lt;code&gt;Not logged in&lt;/code&gt;. The public record only preserves the before-and-after; I do not claim to know which auth step got skipped.&lt;/p&gt;

&lt;p&gt;By number, an execution that stops before authenticating looks cheap. But it did not finish anything. Whoever investigates, retries, and tries a different route pays the fixed startup cost again. Manufacturing cheap failures is more expensive across the run than doing one properly initialized launch.&lt;/p&gt;

&lt;p&gt;Hooks are the same trap. Hooks run small pieces of code on lifecycle events like startup and shutdown. In headless mode you can often skip interactive-flow hooks. But those hooks are usually shared with the interactive session; blanket-disabling them changes normal work too.&lt;/p&gt;

&lt;p&gt;For that reason the change identifies the entrance and branches, instead of turning everything off. The wrapper's guard was narrowed for worker launches and several older hook files were removed. Post-change verification: the hook test suite passed and a live headless call through the wrapper succeeded. Those verifications prove the wrapper and the guard work — they do not prove any reduction rate. Feature-health-check and same-conditions-effect-measurement are different tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist: measure your own environment first
&lt;/h2&gt;

&lt;p&gt;Do not copy someone else's wrapper. Measure your own baseline first — one time — then decide.&lt;/p&gt;

&lt;p&gt;Pick one small consultation you would normally hand to a subagent. Not a benchmark string; the kind of thing you actually use. No file edits, no long research yet. You want the launch fixed cost, not the work.&lt;/p&gt;

&lt;p&gt;For that one call, log input / cache_creation / output / finish_reason. If your platform exposes these values, use them. If not, figure out how to get them &lt;strong&gt;before&lt;/strong&gt; you start optimizing. Do not estimate tokens from the reply's character count. Do not port other people's numbers into your environment's estimate.&lt;/p&gt;

&lt;p&gt;Run the exact same call again through the exact same entrance — same cwd, same loaded config, same appended context, same profile. Compare cache reuse. If the second run does not reuse cache, chase what is varying at the entrance.&lt;/p&gt;

&lt;p&gt;Only now decide a consult profile: the tools you actually need, plus &lt;code&gt;--max-turns&lt;/code&gt; and &lt;code&gt;--max-budget-usd&lt;/code&gt; set from your real numbers — not somebody else's.&lt;/p&gt;

&lt;p&gt;Decide an impl profile the same way, separately. Impl reads, edits, tests; it will be larger. That is expected. Don't widen consult; make a different profile.&lt;/p&gt;

&lt;p&gt;Finally, close the two profiles inside a wrapper and block calls outside the sanctioned path. In this order, you measure current work first, then draw the boundary — instead of drawing the boundary first and breaking the work.&lt;/p&gt;

&lt;p&gt;Short checklist to keep with you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one small everyday consultation.&lt;/li&gt;
&lt;li&gt;Measure input, cache_creation, output, finish_reason for real.&lt;/li&gt;
&lt;li&gt;Run it again through the same entrance and compare reuse.&lt;/li&gt;
&lt;li&gt;Split consult and impl and give each its own limits.&lt;/li&gt;
&lt;li&gt;Repeat under identical model / prompt / tools / cwd / hook conditions.&lt;/li&gt;
&lt;li&gt;Split cold cache vs warm cache, and compare only completed runs.&lt;/li&gt;
&lt;li&gt;Store usage / cost / finish_reason / completion in a result record, separate from the pre-execution row.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Order matters
&lt;/h2&gt;

&lt;p&gt;If you decide caps before you measure, you will only see results shaped by your caps. If you build a wrapper before you measure, you lose the baseline you were going to compare against. The first pass does not need to be pretty. It just needs to preserve the current raw entrance long enough to compare later.&lt;/p&gt;

&lt;p&gt;Don't declare success on one favorable delta. A consult can finish fast by luck. Cache can hit by luck. Build the state where you can &lt;em&gt;repeat&lt;/em&gt; the measurement, then look at the normal range. The pre-change baseline is a starting point, not a permanent benchmark. When jobs or configs change, the normal range moves too.&lt;/p&gt;

&lt;p&gt;When you keep a comparison snapshot, you don't have to publish the entire config file. A name that lets you re-derive the same conditions, plus usage and finish_reason, is enough to serve as a starting point. Don't throw away the whole record just to redact — separate the identifying info you need from the payload you shouldn't keep. Put that boundary in the wrapper too, so recording style doesn't drift per call site.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Did the wrapper actually reduce token cost?
&lt;/h3&gt;

&lt;p&gt;Not something I can claim from this data. Model, request, tools, and hook conditions all moved between the baseline launches and the follow-ups. A same-conditions repeated A/B measurement has not been run. The wrapper's proven value so far is auditability of the entrance (one call path, always-appended caps, one pre-execution row), not a reduction rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not drop &lt;code&gt;--max-turns&lt;/code&gt; / &lt;code&gt;--max-budget-usd&lt;/code&gt; if the job usually finishes early?
&lt;/h3&gt;

&lt;p&gt;Because "usually finishes early" is not the same as "always finishes early." Without the caps, an unexpectedly branching consult (chained errors, expanded scope) can grow to impl-scale spend before you notice. The caps make the boundary explicit for both sides, not only the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why both &lt;code&gt;CLAUDE_CODE_ENTRYPOINT&lt;/code&gt; and &lt;code&gt;CLAUDE_WORKER&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CLAUDE_CODE_ENTRYPOINT=sdk-cli&lt;/code&gt; is what I &lt;em&gt;observed&lt;/em&gt; on headless runs — it marks the CLI/SDK-driven entry, not specifically my wrapper. &lt;code&gt;CLAUDE_WORKER=1&lt;/code&gt; is one I set in the wrapper so downstream hooks can branch on "was this launched by my worker specifically." Two signals, two scopes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I get the wrapper?
&lt;/h3&gt;

&lt;p&gt;Not as a portable script. The current one is tied to my profile names, allow-list, and ledger path. What is worth copying is the shape, not the code: one entrance, an allow-list of overrides, mandatory &lt;code&gt;--max-turns&lt;/code&gt; + &lt;code&gt;--max-budget-usd&lt;/code&gt;, one pre-execution ledger row per call, and an explicit block on raw &lt;code&gt;claude -p&lt;/code&gt; at the caller layer.&lt;/p&gt;

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

&lt;p&gt;The specific flags and env-var names in this article are Claude Code's. The shape — audit-first entrance, allow-listed overrides, mandatory stop conditions per profile, separate pre-execution and result records — is not tied to a specific vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway: this was an audit problem, not a savings problem
&lt;/h2&gt;

&lt;p&gt;Subagents are valuable because they let you delegate without watching every call. That value evaporates the moment you have to pick flags at each launch, hunt logs when something looks off, and hope you did not forget a limit.&lt;/p&gt;

&lt;p&gt;The four pieces the change kept:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Profiles that separate consult from impl.&lt;/strong&gt; Job type is decided ahead of time as a bundle of settings, not improvised per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-turns&lt;/code&gt;.&lt;/strong&gt; A per-job-type completion boundary on a mode that ships with no default limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-budget-usd&lt;/code&gt;.&lt;/strong&gt; A per-execution spend boundary that works even when turn count does not correlate with cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pre-execution ledger.&lt;/strong&gt; One row per launch, so the &lt;em&gt;entrance conditions&lt;/em&gt; are auditable. A result record (tokens, exit code, completion) is the next thing to add.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All four routed through one wrapper (&lt;code&gt;claude-worker.sh&lt;/code&gt;). The takeaway is not a number. It is a procedure: fix your comparison conditions and your completion conditions first, then look at the entrance.&lt;/p&gt;

&lt;p&gt;If your AI-worker consumption feels off, don't start with "which prompt do I trim." Start with: one call, log input + cache_creation + output + finish_reason + completion. Then repeat under identical conditions. Then split cold cache vs warm cache. Only then have you earned the right to change the entrance.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>aiagents</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Jacquard: A Language Where 'What Can This Code Touch?' Fits on One Line</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 25 Jul 2026 12:08:11 +0000</pubDate>
      <link>https://dev.to/nomurasan/jacquard-a-language-where-what-can-this-code-touch-fits-on-one-line-3kpo</link>
      <guid>https://dev.to/nomurasan/jacquard-a-language-where-what-can-this-code-touch-fits-on-one-line-3kpo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I write code and I read code that other people (and models) write. The thing that eats my day is not typing — it's answering &lt;em&gt;what can this code actually touch, and how sure am I?&lt;/em&gt; Jacquard is the first language I've seen that puts the answer on line one and forces the compiler to keep it honest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most languages tell you what a function computes. Very few tell you what it &lt;em&gt;does to the world&lt;/em&gt;. In an era where a growing share of the code I review was written by a model I don't know, that gap is where the day goes: chasing &lt;code&gt;lookup_alias&lt;/code&gt; two files deep to find out whether the innocuous-looking &lt;code&gt;normalize_name&lt;/code&gt; reaches the network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jbwinters/jacquard-lang" rel="noopener noreferrer"&gt;Jacquard&lt;/a&gt; is a small research language (v0.1 rc3 at the time of writing) that takes the opposite bet. Effects — network, files, clock, randomness — live in the type. The checker refuses signatures that hide them. The runtime refuses effects the CLI didn't grant. This post is my first-hand walk through the parts I actually ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-minute install
&lt;/h2&gt;

&lt;p&gt;The install script drops a single binary and a demo tree. No OCaml, no opam.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/jbwinters/jacquard-lang/jacquard-core-0.1-rc3/scripts/install.sh | sh
~/.local/bin/jac run ~/.local/share/jacquard/demos/basics/m1-fact.jac
&lt;span class="c"&gt;# 120&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that prints &lt;code&gt;120&lt;/code&gt;, the language is on your machine and you can follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello, capability grant
&lt;/h2&gt;

&lt;p&gt;Here is the first thing worth seeing. A trivial program prints a string. A slightly less trivial one wants to touch the network — and Jacquard's runtime physically refuses to let it, until I tell it otherwise.&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%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fdevto-jacquard-lang-poc-scene1-2.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%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fdevto-jacquard-lang-poc-scene1-2.png" alt="Jacquard: hello.jac runs, then net-sample.jac is refused without --allow net and accepted with it"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code for the second file is just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;net-sample : (Text) -&amp;gt;{Net} Text
net-sample(url) = match fetch(MkRequest(url, "")) {
  | MkResponse(_, body) -&amp;gt; body
}

net-sample("https://example.com")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice. The signature &lt;code&gt;(Text) -&amp;gt;{Net} Text&lt;/code&gt; says the function may perform the &lt;code&gt;Net&lt;/code&gt; effect. That row is not a comment — the checker computes it from the call graph and rejects any signature that omits an effect the body performs. And at run time, without &lt;code&gt;--allow net&lt;/code&gt;, the program stops with an explicit error before a single packet leaves the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[E0814]: this program requires the `net` effect, which is not granted
  hint: grant it with --allow net, or handle the effect in the program
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 3. Add &lt;code&gt;--allow net&lt;/code&gt; and it runs. That is the whole model: what the type says, the runtime enforces. And it enforces it for dynamically loaded code too, which is where the "reviewed for effects" story of most languages quietly falls apart.&lt;/p&gt;

&lt;p&gt;For me, this is the single most interesting thing about the language. When I skim generated code, the first question I ask is &lt;em&gt;what can this touch?&lt;/em&gt; In Jacquard, the answer is on line one, and I don't have to trust that it's honest — the compiler already checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same policy under three worlds
&lt;/h2&gt;

&lt;p&gt;The install ships a release-risk case study that shows the second big idea: a program written once, then run under different &lt;strong&gt;handlers&lt;/strong&gt; to answer different questions.&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%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fdevto-jacquard-lang-poc-scene3.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%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fdevto-jacquard-lang-poc-scene3.png" alt="Jacquard release-risk demo: the same policy runs under a concrete snapshot handler, a probabilistic telemetry handler with five posterior masses, and Warp property + exhaustive proof lanes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The domain is deliberately mundane: given the health of &lt;code&gt;inventory&lt;/code&gt; and &lt;code&gt;payments&lt;/code&gt;, plus whether there is an error spike, decide &lt;code&gt;Ship&lt;/code&gt;, &lt;code&gt;Canary(percent)&lt;/code&gt;, or &lt;code&gt;Hold(reason)&lt;/code&gt;. What is unusual is how many kinds of question this one policy answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concrete snapshot.&lt;/strong&gt; &lt;code&gt;with-snapshot(Healthy, Degraded, False, ...)&lt;/code&gt; pins the telemetry effect to specific values and returns one decision: &lt;code&gt;canary(5)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probabilistic world.&lt;/strong&gt; &lt;code&gt;with-risk-model&lt;/code&gt; reinterprets the same &lt;code&gt;Telemetry&lt;/code&gt; operations as samples from &lt;code&gt;Dist&lt;/code&gt;, conditions on an observed checkout failure, and enumerates the posterior:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  0.305077  canary(5)
  0.295394  hold("inventory-down")
  0.184621  canary(10)
  0.155903  hold("payments-down")
  0.059005  ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0.059005 ship&lt;/code&gt; is not a threshold to compare against — it's the posterior probability that this policy chooses to ship, given the observed failure. Bayesian evidence, expressed as an ordinary effect operation (&lt;code&gt;observe&lt;/code&gt;) handled by an ordinary handler.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exhaustive proof.&lt;/strong&gt; Warp, the test framework, re-runs the same policy over all 18 telemetry worlds and confirms that when a dependency is &lt;code&gt;Down&lt;/code&gt;, &lt;code&gt;ship&lt;/code&gt; is never chosen. Not sampled — enumerated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zoom in on the &lt;code&gt;inferred authority&lt;/code&gt; block at the top of the screenshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;assessment&lt;/span&gt;      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kt"&gt;Assessment&lt;/span&gt;
&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;           &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;forall&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Telemetry&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;risk&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;         &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;forall&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Telemetry&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Dist&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="n"&gt;conditioned&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;risk&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Dist&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kt"&gt;Decision&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the rows. &lt;code&gt;release-assessment&lt;/code&gt; requires &lt;code&gt;Telemetry&lt;/code&gt;. &lt;code&gt;with-risk-model&lt;/code&gt; consumes &lt;code&gt;Telemetry&lt;/code&gt; and turns it into &lt;code&gt;Dist&lt;/code&gt;. &lt;code&gt;conditioned-release-risk&lt;/code&gt; walks out with only &lt;code&gt;Dist&lt;/code&gt; in its authority. That flow — which effects a piece of code demands, which effects a caller discharges — is not commentary in the margin. It is the language.&lt;/p&gt;

&lt;p&gt;I have written probabilistic models in Python before. The parts that always drift out of sync are: the policy, its mock for testing, and the probabilistic version for what-if analysis. In Jacquard, they are the same 100 lines of code with different handlers on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters when the code was written by a model
&lt;/h2&gt;

&lt;p&gt;I am not an AI-safety researcher. I am someone who spends real hours a week reading code I did not write. Every language I've used treats effects as an emergent property of the call graph — something you infer, at review time, by chasing definitions. That was already an expensive habit. It becomes an unaffordable one when the volume of unfamiliar code goes up.&lt;/p&gt;

&lt;p&gt;Jacquard's bet is that the right unit of review is the signature, and that the signature should already tell you the truth. Two consequences follow, and both showed up in the runs above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-injection-style attacks lose their favorite hiding spot.&lt;/strong&gt; A hidden &lt;code&gt;requests.post&lt;/code&gt; inside a helper called by a helper cannot exist without appearing in the effect row of the caller, all the way up to the entry point. If the entry point isn't launched with &lt;code&gt;--allow net&lt;/code&gt;, the process refuses to run before the payload can trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Same code, different world" becomes ordinary.&lt;/strong&gt; Mock libraries, property-based tests, and probabilistic what-if runs are all handler variants of the one policy. Nothing to keep in sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design lineage is explicit: effect rows and handler discipline from &lt;strong&gt;Koka&lt;/strong&gt;, content-addressed identity and cycle hashing from &lt;strong&gt;Unison&lt;/strong&gt;, hygiene from &lt;strong&gt;Racket&lt;/strong&gt;, &lt;code&gt;-fno-lazy&lt;/code&gt; gut instinct from &lt;strong&gt;Haskell&lt;/strong&gt;. It reads as a careful synthesis, not a rewrite of the wheel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not (yet)
&lt;/h2&gt;

&lt;p&gt;I want to be honest about what I ran:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;v0.1 rc3 is a research prototype. The repo ships a &lt;code&gt;docs/release/0.1/LIMITS.md&lt;/code&gt; and I would take its word for it before shipping this in production.&lt;/li&gt;
&lt;li&gt;The ecosystem is one repository. There is no third-party package story.&lt;/li&gt;
&lt;li&gt;Small syntactic gotchas exist (my first Warp test failed because I hadn't caught a &lt;code&gt;throw&lt;/code&gt; effect I introduced by accident). The error was clear; the fix was to compose with &lt;code&gt;throw.catch&lt;/code&gt;. Not a papercut but not zero learning curve either.&lt;/li&gt;
&lt;li&gt;The install script needs outbound DNS. Sandboxes that block DNS will need a pre-fetched tarball.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these change the shape of the idea. They set the ceiling on how far I'd take it today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in ten minutes
&lt;/h2&gt;

&lt;p&gt;If you want to check the claims above against your own machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/jbwinters/jacquard-lang/jacquard-core-0.1-rc3/scripts/install.sh | sh

&lt;span class="c"&gt;# smoke test&lt;/span&gt;
~/.local/bin/jac run ~/.local/share/jacquard/demos/basics/m1-fact.jac
&lt;span class="c"&gt;# → 120&lt;/span&gt;

&lt;span class="c"&gt;# see the capability gate refuse a net effect&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/net.jac &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
net-sample : (Text) -&amp;gt;{Net} Text
net-sample(url) = match fetch(MkRequest(url, "")) {
  | MkResponse(_, body) -&amp;gt; body
}

net-sample("https://example.com")
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;~/.local/bin/jac run /tmp/net.jac              &lt;span class="c"&gt;# exit 3, E0814&lt;/span&gt;
~/.local/bin/jac run &lt;span class="nt"&gt;--allow&lt;/span&gt; net /tmp/net.jac  &lt;span class="c"&gt;# exit 0, stubbed response&lt;/span&gt;

&lt;span class="c"&gt;# see one policy answer three questions&lt;/span&gt;
sh ~/.local/share/jacquard/demos/case-studies/release-risk/run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command produces the screenshot above on any machine. That is the point of this post — none of what I described is a claim, it is a run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shorter version
&lt;/h2&gt;

&lt;p&gt;If you review code that models write, the first question you learn to ask is &lt;em&gt;what can this touch?&lt;/em&gt; Jacquard is the first language I've used where that question is answered by the signature and enforced by the runtime, in both directions. It is early, it is small, and it is worth an evening.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/jbwinters/jacquard-lang" rel="noopener noreferrer"&gt;github.com/jbwinters/jacquard-lang&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>languages</category>
      <category>effects</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>3 Assumptions That Broke Before I Got gpt-oss-120b Working Through the Anthropic SDK</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Thu, 23 Jul 2026 22:33:31 +0000</pubDate>
      <link>https://dev.to/nomurasan/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic-sdk-m55</link>
      <guid>https://dev.to/nomurasan/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic-sdk-m55</guid>
      <description>&lt;p&gt;I signed up for a free-tier LLM API, hit a credit-card prompt before I'd made a single call, and assumed it meant I was about to get billed. I was wrong about that one. I was not wrong that the SDK code I wrote next would break twice before I got a real response back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: Sakura AI Engine's free tier (3,000 chat completions/month, hosting OpenAI's open-weight &lt;code&gt;gpt-oss-120b&lt;/code&gt;) broke 3 assumptions in a row: that a credit-card prompt on a free plan means billing, that Anthropic's Python SDK authenticates the same way against every compatible endpoint, and that &lt;code&gt;content[0]&lt;/code&gt; in a response is always the text you asked for. Only the last two are real bugs. Both have a one-line fix once you know they exist.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free tier billing&lt;/strong&gt;: Sakura AI Engine's free "foundation model" plan only rate-limits past 3,000 chat completions/month. It does not bill for overage — only a separate, independent pay-as-you-go plan meters and charges. The credit-card prompt at sign-up is identity verification, not a billing trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic SDK &lt;code&gt;401 - Unauthorized&lt;/code&gt;&lt;/strong&gt;: use &lt;code&gt;auth_token="&amp;lt;key&amp;gt;"&lt;/code&gt; instead of &lt;code&gt;api_key="&amp;lt;key&amp;gt;"&lt;/code&gt; when the endpoint expects Bearer auth instead of an &lt;code&gt;x-api-key&lt;/code&gt; header. Same mechanism as Claude Code's &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;'ThinkingBlock' object has no attribute 'text'&lt;/code&gt;&lt;/strong&gt;: don't index &lt;code&gt;resp.content[0]&lt;/code&gt;. Filter instead: &lt;code&gt;[b.text for b in resp.content if b.type == "text"]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Sakura Internet, a Japanese cloud provider, opened a free tier for an API called "AI Engine": 3,000 chat completions and 50 audio-related requests per month, hosting &lt;code&gt;gpt-oss-120b&lt;/code&gt; (OpenAI's open-weight model) behind both an OpenAI-compatible endpoint and an Anthropic-Messages-API-compatible endpoint. Sign-up asked for 5 things in order: a member ID, a project, a phone number verified by SMS, a credit card, and finally an account token in the form &lt;code&gt;&amp;lt;UUID&amp;gt;:&amp;lt;secret&amp;gt;&lt;/code&gt;, shown exactly once.&lt;/p&gt;

&lt;p&gt;The credit card step made me stop. A "free" plan asking for a card reads like a billing trap. It isn't, in this case — the free "foundation model" plan only rate-limits you past 3,000 requests/month, and a separate pay-as-you-go plan is the only one that meters and bills overage. The two plans don't auto-convert into each other; the card is identity verification, not a billing trigger. That part turned out to be a non-issue. What actually blocked me was the code.&lt;/p&gt;

&lt;p&gt;The OpenAI-compatible endpoint worked on the first try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;UUID&amp;gt;:&amp;lt;secret&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ai.sakura.ad.jp/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-oss-120b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, who are you? Introduce yourself in one line.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;ChatGPT&lt;/code&gt;. (&lt;code&gt;gpt-oss-120b&lt;/code&gt; is an open-weight model OpenAI released; its training data appears to include ChatGPT-authored text, which is the likely reason it introduces itself that way.)&lt;/p&gt;

&lt;p&gt;Then I pointed the same account token at the Anthropic-compatible endpoint, using Anthropic's own Python SDK, and swapped in the field name I already knew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;UUID&amp;gt;:&amp;lt;secret&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ai.sakura.ad.jp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-oss-120b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, who are you?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That raised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anthropic.AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same token, same model, same provider account — just a different SDK, and a 401. The Anthropic SDK's &lt;code&gt;api_key=&lt;/code&gt; sends an &lt;code&gt;x-api-key&lt;/code&gt; header by default. Sakura AI Engine's Anthropic-compatible endpoint expects Bearer auth instead, which the SDK only sends when you use &lt;code&gt;auth_token=&lt;/code&gt;. This is the exact mechanism Claude Code itself uses when it's pointed at a gateway through the &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt; environment variable — so once I remembered that, the fix was a one-word field swap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;UUID&amp;gt;:&amp;lt;secret&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# not api_key=
&lt;/span&gt;    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ai.sakura.ad.jp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-oss-120b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, who are you?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returned 200, and printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ThinkingBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;thinking&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;TextBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ChatGPT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is where the second bug showed up. My original code, written before I'd seen this shape, read &lt;code&gt;resp.content[0].text&lt;/code&gt;. &lt;code&gt;gpt-oss-120b&lt;/code&gt; is a reasoning model, and the Anthropic-compatible endpoint puts a &lt;code&gt;ThinkingBlock&lt;/code&gt; first in the &lt;code&gt;content&lt;/code&gt; array, not the answer. &lt;code&gt;ThinkingBlock&lt;/code&gt; has no &lt;code&gt;.text&lt;/code&gt; attribute, so that line threw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AttributeError: 'ThinkingBlock' object has no attribute 'text'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to stop assuming position and filter by type instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;text_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;text_blocks&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(no text block)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That printed &lt;code&gt;ChatGPT&lt;/code&gt; — the same answer the OpenAI-compatible endpoint gave, confirming both endpoints were routing to the same model the whole time. The auth header and the response shape were the only things standing in the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three blockers, three fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;blocker&lt;/th&gt;
&lt;th&gt;what broke&lt;/th&gt;
&lt;th&gt;the fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Credit card on a free plan&lt;/td&gt;
&lt;td&gt;Looked like a billing trigger before any call was made&lt;/td&gt;
&lt;td&gt;Non-issue: free "foundation model" plan only rate-limits past 3,000 req/month; the metered plan is separate and doesn't auto-activate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic SDK auth&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api_key=&lt;/code&gt; → &lt;code&gt;AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auth_token=&lt;/code&gt; (Bearer auth — same mechanism as Claude Code's &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning model response shape&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;resp.content[0].text&lt;/code&gt; → &lt;code&gt;AttributeError: 'ThinkingBlock' object has no attribute 'text'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[b.text for b in resp.content if b.type == "text"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row cost time worrying, not debugging. The other two cost actual stack traces, and both trace back to the same root cause once you line them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;Call it &lt;strong&gt;the compatibility mirage&lt;/strong&gt;: an endpoint that accepts the same request shape and the same model name as the SDK you already know isn't necessarily compatible with everything that SDK assumes. The parts that silently differ are the parts you can't see in a quick glance at the docs — the auth handshake and the response schema.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Matching the URL and the payload shape isn't enough. You also have to match how the client authenticates and how it parses what comes back — and a "compatible" endpoint can diverge on both without ever changing the request you send.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd guess this isn't unique to Sakura AI Engine. Any provider exposing an Anthropic-Messages-API-shaped endpoint with Bearer auth will hit the same 401 until &lt;code&gt;auth_token=&lt;/code&gt; replaces &lt;code&gt;api_key=&lt;/code&gt;. And any client code written against &lt;code&gt;content[0]&lt;/code&gt; will break the moment a reasoning model puts a &lt;code&gt;ThinkingBlock&lt;/code&gt; first — regardless of which provider is serving it.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Does Sakura AI Engine charge me if I go over the free 3,000 requests/month?
&lt;/h3&gt;

&lt;p&gt;No, not on the free "foundation model" plan — you get rate-limited, not billed. Only a separate pay-as-you-go plan meters and charges for overage, and the two plans don't auto-convert into each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the Anthropic Python SDK return a 401 against a third-party endpoint even though the key is valid?
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;api_key=&lt;/code&gt; sends an &lt;code&gt;x-api-key&lt;/code&gt; header by default, and endpoints that expect Bearer auth (&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;) need &lt;code&gt;auth_token=&lt;/code&gt; instead. This is the same mechanism Claude Code uses through the &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt; environment variable when talking to a gateway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is &lt;code&gt;resp.content[0]&lt;/code&gt; not always the text response?
&lt;/h3&gt;

&lt;p&gt;Reasoning models such as &lt;code&gt;gpt-oss-120b&lt;/code&gt; can return a &lt;code&gt;ThinkingBlock&lt;/code&gt; as the first element of the &lt;code&gt;content&lt;/code&gt; array instead of the answer. &lt;code&gt;ThinkingBlock&lt;/code&gt; has no &lt;code&gt;.text&lt;/code&gt; attribute, so indexing position 0 and reading &lt;code&gt;.text&lt;/code&gt; throws &lt;code&gt;AttributeError&lt;/code&gt;. Filter by &lt;code&gt;block.type == "text"&lt;/code&gt; instead of assuming index 0 is the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do the OpenAI-compatible and Anthropic-compatible endpoints return the same model output?
&lt;/h3&gt;

&lt;p&gt;Yes — in this case both endpoints routed to the same underlying &lt;code&gt;gpt-oss-120b&lt;/code&gt; model and returned the same text (&lt;code&gt;ChatGPT&lt;/code&gt;) once the auth header and the response parsing were both fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself: a 3-step checklist
&lt;/h2&gt;

&lt;p&gt;If you're integrating a reasoning model behind any Anthropic-Messages-API-compatible endpoint:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grep your own client code for &lt;code&gt;content[0]&lt;/code&gt; or &lt;code&gt;.content[0].text&lt;/code&gt;. If you find it, you have the same latent bug this article does — it just hasn't fired yet because your model hasn't returned a thinking block first.&lt;/li&gt;
&lt;li&gt;If a third-party endpoint 401s against Anthropic's SDK with a key that works fine elsewhere, try &lt;code&gt;auth_token=&lt;/code&gt; before assuming the key itself is wrong.&lt;/li&gt;
&lt;li&gt;Replace any &lt;code&gt;content[0]&lt;/code&gt; assumption with a type filter: &lt;code&gt;[b.text for b in resp.content if b.type == "text"]&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Minimal repro, end to end: save the fixed &lt;code&gt;auth_token=&lt;/code&gt; client code above as &lt;code&gt;sakura_hello.py&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;anthropic
python sakura_hello.py
&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;$ python sakura_hello.py
[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]
ChatGPT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these three took more than a few minutes to fix once I found them. All three would have been avoidable with five minutes of reading the response schema first — which is exactly the five minutes I skipped.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I test with a real API key before I trust what the docs imply. Verified end-to-end against Sakura AI Engine's live API and a real account token.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an adapted English rewrite of an essay I first wrote in Japanese, on Qiita. I worked with AI to shape and restructure the piece for a dev.to audience; the code, the errors, and the conclusions are my own, verified against a live API key.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>llm</category>
      <category>anthropic</category>
    </item>
    <item>
      <title>I built a schemaless intake for my CRM. It silently dropped files three times.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:30:19 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-built-a-schemaless-intake-for-my-crm-it-silently-dropped-files-three-times-2po7</link>
      <guid>https://dev.to/nomurasan/i-built-a-schemaless-intake-for-my-crm-it-silently-dropped-files-three-times-2po7</guid>
      <description>&lt;p&gt;I dropped a file into my own intake folder, ran the sync, and watched it finish clean. No error. No warning. The file was never there again — not in the index, not in any downstream output, not anywhere the system could tell me about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The risk of a freeform intake was never "anything can get in." It's that nothing tells you what got dropped.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A schema-less intake that lets you dump any file into a folder doesn't fail loudly when a file falls out of the pipeline — it just finishes normally without it. I hit that exact hole three times while prototyping a personal CRM over a weekend. The fix wasn't more validation at the intake. It was a deterministic index layer, sitting between intake and interpretation, that records what it saw and nothing else — so "did this file get picked up" becomes a question you can check mechanically instead of one you have to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer: why does a freeform data intake lose files silently, and how do you catch it?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A freeform intake (drop any file, decide the schema later) doesn't reject bad input — it just never validates the input at all. An unrecognized directory name, a missing metadata field, or a naive downstream reader can all cause a file to disappear from processing with zero errors and zero warnings.&lt;/li&gt;
&lt;li&gt;I hit three separate versions of this while building a personal case-tracking intake: an unknown client directory that passed through silently, a missing date field that silently fell out of a downstream calculation, and a keyword-matching classifier that misfired on my own test data — twice.&lt;/li&gt;
&lt;li&gt;The fix is a &lt;strong&gt;deterministic index layer&lt;/strong&gt; between the freeform intake and any interpretation step (LLM or human): it records path, date, and a content hash for every file it sees, does zero interpretation, and therefore produces the identical output for the identical input every time. "What got picked up and what didn't" becomes something you diff, not something you assume.&lt;/li&gt;
&lt;li&gt;Four follow-up experiments (Plan A vs. Plan B for where to stop a bad sync, replacing keyword matching with an LLM, catching downstream code that ignores a health flag, and caching interpretation results) show where that determinism can quietly break — and how to keep it from breaking.&lt;/li&gt;
&lt;li&gt;This is a hypothesis-stage design. It has been run end-to-end against a local mock repository, not against production case data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I want to be upfront about scale before anything else: this is a weekend prototype for my own case-tracking workflow, not a production data platform. But the failure mode underneath it — a permissive intake that can drop input without telling you — shows up at any scale where "let the AI read whatever's in the folder" sounds like a reasonable starting design. I built one of those. It looked like it worked. It quietly wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built a schemaless intake in the first place
&lt;/h2&gt;

&lt;p&gt;My case notes, meeting minutes, and Slack threads for each client pile up scattered across folders and chats. Answering "what phase is this client in right now" meant opening files and re-reading them by hand, every time. That manual lookup was the whole problem I was trying to solve.&lt;/p&gt;

&lt;p&gt;I'd tried the opposite approach before: design the schema first. I started from "what fields does every client record need," and ran straight into the fact that clients are different enough that no single schema covered all of them. I stalled out at the design stage and never shipped anything.&lt;/p&gt;

&lt;p&gt;This time I deferred the schema decision instead of trying to resolve it up front. I prototyped a freeform intake over a weekend — drop case notes into a folder with no fixed shape, and let a later interpretation layer decide what they mean. The reference point was Palantir's ontology model — specifically just the idea of separating meaningful data units (Object), the connections between them (Link), and the operations that change state (Action). I only borrowed that three-way split; nothing else about the ontology system.&lt;/p&gt;

&lt;p&gt;What I learned building it: accepting "anything" without a schema wasn't the dangerous part. The dangerous part was that a file I thought I'd successfully dropped into the intake could fall out of processing somewhere downstream, with no path back to noticing it had happened. I hit that same shape of hole three separate times once I started actually running the thing.&lt;/p&gt;

&lt;p&gt;To be clear about where this stands: this is still a hypothesis-stage design with no production case data run through it. It's been exercised end-to-end against a local mock case repository — not more than that. If you're the kind of person stuck at the schema-design step of a system meant to feed an LLM everything, that's who this is written for. It's who I was a few months ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design: a deterministic index layer that never interprets anything
&lt;/h2&gt;

&lt;p&gt;My first instinct was something close to "drop files in a per-client folder, let an AI read the contents and figure out the client's state." That design has one property that kills it: nobody notices the file the AI failed to read. LLM interpretation drifts run to run, misses things, and doesn't throw an exception when it misses.&lt;/p&gt;

&lt;p&gt;So I put a layer between intake and interpretation that does zero interpretation of its own. It's close in spirit to a small data lake with schema-on-read — nothing is validated or typed at write time. The system splits into three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Intake&amp;lt;br/&amp;gt;data/intake/&amp;amp;lt;client_id&amp;amp;gt;/&amp;lt;br/&amp;gt;drop files, no schema enforced"]
    B["Deterministic index layer&amp;lt;br/&amp;gt;records path, date, source_id, health&amp;lt;br/&amp;gt;zero interpretation"]
    C["Interpretation layer (LLM/human)&amp;lt;br/&amp;gt;client phase, issues, next action&amp;lt;br/&amp;gt;judgment happens here"]
    D["last_contact_at&amp;lt;br/&amp;gt;computed mechanically from frontmatter dates"]
    E["Interpretation cache&amp;lt;br/&amp;gt;separate file, keyed by path&amp;lt;br/&amp;gt;never written back to the index"]

    A --&amp;gt; B
    B --&amp;gt; C
    B --&amp;gt; D
    D --&amp;gt; C
    C --&amp;gt; E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intake&lt;/strong&gt;: drop files under &lt;code&gt;data/intake/&amp;lt;client_id&amp;gt;/&lt;/code&gt;. The directory name is the client ID, so there's no separate entity-resolution step to write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic index layer&lt;/strong&gt;: records every file's path, date, and &lt;code&gt;source_id&lt;/code&gt; (a content hash) to JSONL. No interpretation happens here, so the identical input always produces the identical index. Whether a file was picked up or not is something you can verify by reading this layer's own record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interpretation layer&lt;/strong&gt;: an LLM or a human decides the client's phase, open issues, and next action. The one exception is "last contact date" — that's computed mechanically from the index layer's frontmatter dates, not left to the LLM. Handing date freshness to an LLM invites hallucinated recency judgments, so that stays on the deterministic side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the deterministic index layer is actually built (path, date, source_id)
&lt;/h2&gt;

&lt;p&gt;The code below is a generalized, simplified version of the real implementation — it won't run as-is. What I want to convey is the pattern, not a drop-in library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;frontmatter&lt;/span&gt;  &lt;span class="c1"&gt;# standing in for python-frontmatter
&lt;/span&gt;
&lt;span class="n"&gt;INTAKE_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/intake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known_client_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;client_dir&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INTAKE_ROOT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterdir&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known_client_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[warn] unknown client_id &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Add it to config or check the directory name.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[warn] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has no date key. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It will not be reflected in last_contact_at.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INTAKE_ROOT&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;known_client_ids&lt;/code&gt; is held up front, and any directory name not in it triggers a warning. A file with no date key is not crashed or skipped — it goes into the index as &lt;code&gt;date: None&lt;/code&gt;, but with a warning attached. Neither of those two warnings existed in the first version. I added both after stepping in the holes below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local end-to-end verification (idempotency checked with sha256, not eyeballing)
&lt;/h2&gt;

&lt;p&gt;Before touching anything close to real case data, I built a mock case repository locally and ran the whole thing through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repository scan → copy files into &lt;code&gt;intake&lt;/code&gt; (a sync script)&lt;/li&gt;
&lt;li&gt;Index generation (the code above)&lt;/li&gt;
&lt;li&gt;Reading the index down to "today's tasks"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The sync script compares content by sha256 whenever a same-named file already exists at the destination, and skips the write if there's no difference. Run the same script twice in a row and the second run finishes with zero diffs. Confirming idempotency by hash comparison instead of scanning a log by eye was a small thing, but it was reassuring.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Skip the write if the destination already has identical content.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# unchanged
&lt;/span&gt;    &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only decide whether to run this against real case data after this whole rehearsal passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole 1: an unknown client_id directory passed through with zero errors
&lt;/h2&gt;

&lt;p&gt;The first version treated &lt;code&gt;client_dir.name&lt;/code&gt; as the client ID unconditionally. My assumption was: as long as the scan finishes without an error, the intended client's files must have been picked up correctly.&lt;/p&gt;

&lt;p&gt;I typo'd a client ID, made a directory with that wrong name, dropped a file in it, and ran the sync. It finished cleanly — no error, no warning. Opening the index file, that file wasn't in it. As far as the system was concerned, a file I'd physically placed had never existed.&lt;/p&gt;

&lt;p&gt;I'd been treating "finished without an error" and "was processed as intended" as the same statement. They weren't. The scan never validated the directory name at all — an unknown name passed through exactly like a known &lt;code&gt;client_id&lt;/code&gt; would. The missing piece was a check against the known set of valid client IDs, and that check simply wasn't in the original design. I added the &lt;code&gt;known_client_ids&lt;/code&gt; check and the warning to &lt;code&gt;build_index&lt;/code&gt; in response: something like &lt;code&gt;unknown client_id 'mystery-client'. Add it to config or check the directory name.&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole 2: files with no date key silently never reached the freshness calculation
&lt;/h2&gt;

&lt;p&gt;I'd defined the index layer's job as "record that a file exists." A file missing its date key, recorded as &lt;code&gt;date: null&lt;/code&gt;, felt sufficient — whoever read the index could make the call themselves. Being in the index at all was supposed to be proof the file had been "properly picked up."&lt;/p&gt;

&lt;p&gt;I dropped a file with no date key in its frontmatter and checked. As expected, it landed in the index as &lt;code&gt;date: null&lt;/code&gt; — no crash, no skip. But it was excluded from the last-contact-date (&lt;code&gt;last_contact_at&lt;/code&gt;) calculation, and looking at the index gave no way to notice that exclusion had happened.&lt;/p&gt;

&lt;p&gt;I'd been conflating "is in the index" with "is used by the interpretation layer's calculations." They aren't the same state. The index is a record of existence; it makes no promise about how downstream calculations will use that value. I hadn't been thinking of those as two separate things, and that's exactly what I'd missed. I added a warning for date-less files — "will not be reflected in the last_contact_at calculation" — so the distinction is visible the moment you look at the index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole 3: a keyword-matching phase classifier misfired on my own test data — twice
&lt;/h2&gt;

&lt;p&gt;I'd built a mock extractor for client phase using plain keyword matching. I wrote a test-data note that said "this is dummy data for a PoC verification test," and the string "PoC" tripped the classifier into misjudging &lt;code&gt;phase=prototype/demo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I changed the wording to "not a production log" and re-tested — this time the string "production" tripped it into misjudging &lt;code&gt;phase=full production&lt;/code&gt;. The second attempt was supposed to be a lesson learned from the first, and I fell into the identical shape of hole again.&lt;/p&gt;

&lt;p&gt;This is the design principle I'd already committed to — never let keyword matching do interpretation, keep index and interpretation separate — proving itself twice against my own test data. Phase classification is exactly the kind of judgment that belongs to an LLM or a human, not string matching; the mock extractor was underbuilt on that front.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a freeform intake actually risks (the working hypothesis)
&lt;/h2&gt;

&lt;p&gt;None of the three holes were the kind of bug that crashes and stops the system. They were the opposite: processing completed with no error and no warning, in a state that wasn't actually correct. Building this made the real risk concrete for me — a freeform intake's danger isn't "anything can get in." It's that the processing result gives you no way to tell what got picked up and what got dropped.&lt;/p&gt;

&lt;p&gt;The deterministic index layer exists to make that indistinguishability mechanically checkable. Because it does no interpretation, identical input always produces an identical result. That means "were this client's files picked up" and "is this date reflected in the freshness calculation" are both questions you can answer by reading the index file.&lt;/p&gt;

&lt;p&gt;That said, everything above is a mock-repository trial. Real case data will be messier in format than anything I've tested, and I expect there are holes in here I haven't found yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment: where should a warning threshold stop the sync?
&lt;/h2&gt;

&lt;p&gt;An earlier draft of this design left an open concern: logging warnings from the index layer doesn't change the fact that you still have to go look at the log. Here I actually ran two candidate designs for what happens once the warning count crosses a threshold, and compared them directly. Plan A aborts the scan mid-way and exits with &lt;code&gt;exit(1)&lt;/code&gt; the moment the threshold is crossed. Plan B lets index generation run to completion, sets an &lt;code&gt;unhealthy&lt;/code&gt; flag, and leaves it to the downstream sync step to check that flag and refuse the handoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction:&lt;/strong&gt; Plan A, which stops processing the instant a warning is detected, should be the safer of the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; with the threshold set at 2, a third warning (an unknown &lt;code&gt;client_id&lt;/code&gt;) triggered mid-scan. Plan A aborted right there. Two normal, zero-warning files belonging to a different client — files that would have been scanned later — never appeared in the index at all. Opening the index file on its own gives you no way to tell whether it reflects every file or stopped partway through. Plan B kept scanning to the end with the same three warnings, wrote all six entries to the index, and recorded &lt;code&gt;unhealthy: true&lt;/code&gt; in a &lt;code&gt;meta.json&lt;/code&gt; sidecar. The downstream sync refused the handoff on seeing that flag — but the index itself still held a complete record of every file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; index.jsonl
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;meta.json
&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;6 index.jsonl
{"warning_count": 3, "unhealthy": true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Analysis:&lt;/strong&gt; stopping the "process" of index generation and keeping the index's "record" complete turned out to be incompatible goals. Once the index layer is defined as zero-interpretation-plus-complete-record, Plan A — which injects a judgment call mid-generation and cuts it short — contradicts that definition. A warning-threshold "stop" decision belongs at the boundary between the index and downstream processing, not inside the index layer itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intake_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;known_client_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_warnings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;client_dir&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intake_root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterdir&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# scan logic runs to completion, never aborted mid-way
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unhealthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_warnings&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unhealthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index unhealthy: refuse to sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the stop decision lives at the boundary
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Experiment: does swapping keyword matching for an LLM break the index's determinism?
&lt;/h2&gt;

&lt;p&gt;The other open concern: the moment keyword-based phase detection gets replaced with an actual LLM call, does the whole index lose its claim to reproducibility?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction:&lt;/strong&gt; index generation and interpretation (phase detection) are separate functions, but since both get called inside the same pipeline, I expected the non-determinism on the interpretation side to bleed into the index side too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; running index generation twice, as an independent process, against identical input, and comparing the sha256 of the two &lt;code&gt;index.jsonl&lt;/code&gt; outputs — they matched exactly. Then I swapped in a deliberately non-deterministic mock that reads the index and returns a phase (standing in for an LLM call, using an unseeded random number to change its answer every run) and ran that twice: all three subjects' phase results changed between runs. The index didn't change. The interpretation did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analysis:&lt;/strong&gt; the index records the LLM's input (file contents) as-is; the interpretation layer reads that record and never writes back into the index. Data only flows from index toward interpretation, never the other way, so no matter how wide the interpretation side's randomness swings, there's no path for it to reach the index. My concern — "putting an LLM in the loop breaks reproducibility" — was correct, but what breaks is the reproducibility of the interpretation, not of the index. Because the index completely records what was handed to the LLM, even if the interpretation's result changes on every run, which input produced which judgment can still be verified after the fact by reading the index.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intake_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# no randomness, no LLM call. records the read content as-is
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...}]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# stands in for the LLM call. never writes back into the index
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;call_llm_phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;index_entries&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Experiment: how do you catch downstream code that ignores the unhealthy flag?
&lt;/h2&gt;

&lt;p&gt;Plan B, from two sections up, assumed downstream processing would always check the &lt;code&gt;unhealthy&lt;/code&gt; flag. I tested what happens when an implementation slips through that doesn't — and whether that gap itself is detectable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction:&lt;/strong&gt; even with the current design of recording &lt;code&gt;unhealthy&lt;/code&gt; in a sidecar &lt;code&gt;meta.json&lt;/code&gt;, an implementation that doesn't reference it should still get caught somehow — at minimum, the index file's shape not matching expectations should throw an error somewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; I wrote a naive downstream reader that never references &lt;code&gt;meta.json&lt;/code&gt; at all, just parses each line of &lt;code&gt;index.jsonl&lt;/code&gt; as an entry directly, and ran it. It completed normally with zero warnings and printed out the healthy-looking last-contact date as if nothing were wrong. &lt;code&gt;unhealthy: true&lt;/code&gt; never surfaced anywhere in this downstream code's output. Then I added a single sentinel record — &lt;code&gt;{"_sentinel": true, "_health": "unhealthy", ...}&lt;/code&gt; — as the first line of the index JSONL and fed the same naive reader the same file. It crashed with a &lt;code&gt;KeyError&lt;/code&gt; on &lt;code&gt;entry["client_id"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analysis:&lt;/strong&gt; a sidecar &lt;code&gt;meta.json&lt;/code&gt; only does its job for an implementation that intentionally chooses to reference it. For an implementation that doesn't go looking, the record living there might as well not exist. A sentinel record instead mixes health information into the index itself — the one file downstream code is guaranteed to read. An implementation that ignores the flag doesn't get to "successfully ignore" it anymore; it gets stopped by a record shape it wasn't expecting, and that failure to ignore becomes visible on its own. Carrying health status as part of the index's own data, instead of a sidecar, doesn't depend on how carefully downstream code was written.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A naive downstream reader unconditionally reads entry["client_id"].
# A meta.json sidecar completes without noticing.
# Feeding it an index with a sentinel record
# ({"_sentinel": True, "_health": "unhealthy"}) as the first line
# raises a KeyError on that line and crashes instead.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;index_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Experiment: does caching interpretation results back into the index break determinism?
&lt;/h2&gt;

&lt;p&gt;One more thing I wanted to confirm: if phase-detection results get cached by writing them back into the index, does the mechanism itself — "an sha256 match on the index proves the input is identical" — stop working?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction:&lt;/strong&gt; index generation and interpretation are separate functions, but writing interpretation results back into each index entry and keeping everything in one file should make the latest interpretation visible just by looking at the index, and should be more convenient to work with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; with an implementation that adds a &lt;code&gt;phase&lt;/code&gt; field to each index entry and overwrites the file, running twice against identical input produced two completely different sha256 values for &lt;code&gt;index.jsonl&lt;/code&gt;. Diffing the two showed &lt;code&gt;content&lt;/code&gt; unchanged — the &lt;code&gt;phase&lt;/code&gt; value written back had simply changed between runs. I then separated &lt;code&gt;phase&lt;/code&gt; into its own file (&lt;code&gt;phase_cache.jsonl&lt;/code&gt;), with the index never written to at all, and ran it twice: &lt;code&gt;index.jsonl&lt;/code&gt;'s sha256 matched exactly both times, while &lt;code&gt;phase_cache.jsonl&lt;/code&gt;'s sha256 differed between runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analysis:&lt;/strong&gt; writing interpretation results back into the index makes it impossible to tell, from a sha256 match or mismatch alone, whether the index's own contents (path, date, source_id) changed, or whether only the interpretation changed while the input stayed the same. Once the index is defined as zero-interpretation-plus-deterministic, importing interpretation's non-determinism breaks that definition the moment it happens. Separating the cache into a file independent of the index keeps the index's sha256 usable as a signal of input identity.&lt;/p&gt;

&lt;p&gt;There was a second hole hiding in this one, though: keying the cache on &lt;code&gt;source_id&lt;/code&gt; alone. Two files with byte-identical content (a reused template, for instance) placed under different client directories produce the same &lt;code&gt;source_id&lt;/code&gt; by construction. Look that up in the cache and one client's interpretation result can silently overwrite the other's. Keying on &lt;code&gt;path&lt;/code&gt; instead — which is always unique within the index — avoids that collision entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4a: written back into the index (index.jsonl's sha256 changes every run)
&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# overwrites index.jsonl itself
# 4b: kept in a separate file, keyed on path (always unique within the index)
&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# index.jsonl is never touched
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion: a three-layer design that makes freeform intake verifiable
&lt;/h2&gt;

&lt;p&gt;Across the three holes and the four experiments above, the design settled into this shape. Intake (&lt;code&gt;data/intake/&amp;lt;client_id&amp;gt;/&lt;/code&gt;) is a schema-free layer — drop a file and it's tied to a client ID by directory name alone. The index does zero interpretation and completely records every file's path, date, and &lt;code&gt;source_id&lt;/code&gt;; health status (&lt;code&gt;unhealthy&lt;/code&gt;) lives inside the index's own data, as a sentinel record, not in a sidecar. Interpretation is where an LLM or a human judges client phase and next actions, and that result never gets written back into the index — it goes into a separate cache file keyed on &lt;code&gt;path&lt;/code&gt;, which is always unique within the index.&lt;/p&gt;

&lt;p&gt;These three layers, plus two boundary rules (health status lives inside the index's data; interpretation results never write back into the index), are where this design landed. It has not yet been run against real case data. Operational monitoring and handling the format variance real case data will inevitably have are both out of scope for this post — but the chain of verification that started from "the hole in a schema-free intake" feels, to me, like it's made one full pass through the question of what belongs at which point across intake, index, and interpretation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste checklist: before you build a freeform intake
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# does anything in your intake pipeline finish "success" without validating&lt;/span&gt;
&lt;span class="c"&gt;# the identifier it just consumed?&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;"iterdir&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;listdir&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;glob("&lt;/span&gt; your_intake_script.py

&lt;span class="c"&gt;# does your index (or equivalent record) get written to by more than one layer?&lt;/span&gt;
&lt;span class="c"&gt;# if interpretation results and raw intake facts share a file, you've lost the&lt;/span&gt;
&lt;span class="c"&gt;# ability to tell "input changed" from "interpretation changed" by hashing it&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;"&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;jsonl&lt;/span&gt;&lt;span class="se"&gt;\"\|\.&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; your_index_script.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each hit, ask one question: &lt;strong&gt;if this component silently drops or silently reinterprets something, is there any file you could open afterward that would tell you?&lt;/strong&gt; If the answer is no, you have the same hole this post describes.&lt;/p&gt;

&lt;p&gt;If you're tempted to skip the index layer because "the LLM can just read everything directly" — that was my first design too, and it's the design that hides exactly this failure. A misread file produces no exception. Nothing tells you it happened, ever, unless something downstream of the LLM keeps its own zero-interpretation record of what went in.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Why not just let the LLM (or a human) read every file directly, and skip the index layer?
&lt;/h3&gt;

&lt;p&gt;That was my first design, and it's the one that hides this exact failure. A file the LLM fails to read produces no exception — its interpretation just quietly doesn't happen. Without a layer that records "this file existed and was seen" independently of interpretation, there's no way to notice a miss after the fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doesn't validating &lt;code&gt;known_client_ids&lt;/code&gt; defeat the point of calling this a freeform intake?
&lt;/h3&gt;

&lt;p&gt;No — "freeform" here only applies to file content and schema, not to the directory namespace. The intake still needs a closed, known set of valid client IDs, because without one, a typo in a directory name is silently treated as "there is no such client" rather than as an error. That's Hole 1 in this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  If the index never interprets anything, how does it produce &lt;code&gt;last_contact_at&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Mechanically, from the date field in each file's frontmatter — not from an LLM judgment call. Handing "is this recent" to an LLM risks an inconsistent, hallucinated read on freshness across runs, so that one calculation stays on the deterministic side of the boundary, even though everything else about client state is interpretation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is any of this running against real client data yet?
&lt;/h3&gt;

&lt;p&gt;No. Everything described here — the three holes and the four experiments — was run against a local mock case repository, end-to-end, not production data. Real case files will vary in format more than anything I've tested so far, and I expect to find holes this design doesn't yet handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I verified this
&lt;/h2&gt;

&lt;p&gt;Every claim above came from actually running the code, not from reasoning about what it should do. The three holes were reproduced by hand: creating a mistyped &lt;code&gt;client_id&lt;/code&gt; directory, dropping a file with no date field, and feeding the mock phase classifier test data containing "PoC" and then "production" as literal strings. The four follow-up experiments were run as described — sha256 comparisons on &lt;code&gt;index.jsonl&lt;/code&gt; across repeated runs, a deliberately non-deterministic mock standing in for an LLM call, a naive downstream reader tested both with and without a sentinel record, and two cache designs (written-back vs. separate file) diffed by hash. All of it ran against a local mock case repository built for this purpose; none of it has touched real case data. Client names, directory structures, and specific case content are my own test fixtures, not real client information.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.palantir.com/docs/foundry/ontology/core-concepts" rel="noopener noreferrer"&gt;Ontology – Core concepts | Palantir&lt;/a&gt; — source for the Object / Link / Action split used to separate meaning from process in this design&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). Originally written in Japanese; the English here is an AI-assisted re-adaptation of my own text, not a literal translation. The design, the code, and the experiment results are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>softwaredesign</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your GitHub Release URL Opens Fine. That's Not Proof You Can Get the Files Back</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Wed, 22 Jul 2026 04:06:38 +0000</pubDate>
      <link>https://dev.to/nomurasan/your-github-release-url-opens-fine-thats-not-proof-you-can-get-the-files-back-6jk</link>
      <guid>https://dev.to/nomurasan/your-github-release-url-opens-fine-thats-not-proof-you-can-get-the-files-back-6jk</guid>
      <description>&lt;p&gt;I keep the images for my blog in GitHub Releases. Not software builds — PNGs. Article illustrations, avatar assets, that kind of thing, attached to releases as GitHub Release assets and served straight from &lt;code&gt;releases/download/...&lt;/code&gt; URLs.&lt;/p&gt;

&lt;p&gt;It works well: the release URLs open, the asset lists render, and the illustrations in my published articles load from those URLs every day.&lt;/p&gt;

&lt;p&gt;Then I read my own automation logs for a second release I use for avatar assets: &lt;code&gt;download=failure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The URL for that release opens fine too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: "Upload succeeded" and "a consumer can get the same bytes back" are different claims, verified by different checks. A release URL proves the box exists, not that your files are in it. The fix is a 5-step round-trip check — URL, asset list, re-download through the consumer's path, SHA-256 comparison, ledger — designed around where to &lt;em&gt;stop&lt;/em&gt; when something fails, not around the happy path.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Before you call a release "done", walk these five stages in order and stop at the first one that fails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expectation first&lt;/strong&gt;: decide which file, in which role (editable original / preview / compressed distribution), in which format you need back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asset list&lt;/strong&gt;: query the release's assets via the API and compare &lt;code&gt;name&lt;/code&gt; / &lt;code&gt;state&lt;/code&gt; / &lt;code&gt;size&lt;/code&gt; against your expectation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-download&lt;/strong&gt;: fetch the assets through the same path a consumer would use, into an empty directory. Never inspect the folder you uploaded from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SHA-256&lt;/strong&gt;: hash the canonical original and the round-tripped copy, and compare across that boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ledger&lt;/strong&gt;: record what each check actually verified — not a single "done" flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A failure at any stage parks the process &lt;em&gt;at that stage&lt;/em&gt;. You don't advance, and you don't quietly switch to a different file or release to make the check pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  A release URL is a shipping label, not the contents
&lt;/h2&gt;

&lt;p&gt;In upload automation, "create the release" and "upload the assets" are separate steps — separate API calls, often separate script lines. The first can succeed while the second fails on permissions, a filename mismatch, or a plain network error. When that happens you get a perfectly valid release URL attached to a box that's missing contents. My own workflow logs include runs where the release-creation step reported success and an asset-upload step after it did not.&lt;/p&gt;

&lt;p&gt;If your definition of done is "the URL came back", you will ship empty boxes and not know it. The URL opening is evidence that the box exists. That's all it's evidence of.&lt;/p&gt;

&lt;p&gt;There's a second, quieter problem: the same artifact usually exists in several forms. The editable original, a preview, a resized copy, a compressed distribution — in a browser they can look identical. Naming conventions (&lt;code&gt;original&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;preview&lt;/code&gt;) help as an index, but nobody enforces them, and they guarantee nothing about the bytes. Re-saving a PNG through a different tool changes its compression and metadata while the pixels look the same. Rebuilding a ZIP with the same files produces different bytes if timestamps or ordering changed. A copy that went through an image CDN or a chat app may be a resized or re-encoded version of what you sent. Filenames and eyeballs cannot settle "is this the same file". Hashes can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Fix your expectation before you check anything
&lt;/h2&gt;

&lt;p&gt;The first thing to verify is not the URL. It's your own claim: &lt;em&gt;which&lt;/em&gt; file, in &lt;em&gt;which&lt;/em&gt; role, in &lt;em&gt;which&lt;/em&gt; format do I need to get back?&lt;/p&gt;

&lt;p&gt;Skip this and the check degrades into validating whatever happens to be there. If the release holds a preview and you needed the editable original, every downstream check can pass while verifying the wrong thing. Write the expectation down first — filename, role, format — and only then look at what the release actually holds.&lt;/p&gt;

&lt;p&gt;If several releases could plausibly be the home of your canonical files (a trial-distribution one, a test one, an archive one), pick by the role each release has actually served, not by which has the friendliest name. And before adding a missing file to whichever release is closest at hand, check whether the real canonical home already exists — quietly promoting a scratch release to "canonical" changes its meaning for everyone who used it before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: List what the server claims to have
&lt;/h2&gt;

&lt;p&gt;GitHub calls files attached to a release &lt;em&gt;assets&lt;/em&gt;, and the &lt;a href="https://docs.github.com/en/rest/releases/assets" rel="noopener noreferrer"&gt;release assets REST API&lt;/a&gt; returns them machine-readably:&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 &lt;span class="s2"&gt;"repos/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;/releases/tags/&amp;lt;release-tag&amp;gt;"&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;'.assets[] | {name, state, size}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the docs, each asset carries a &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;state&lt;/code&gt;, a &lt;code&gt;size&lt;/code&gt;, a &lt;code&gt;browser_download_url&lt;/code&gt;, and a &lt;code&gt;digest&lt;/code&gt;. Check three things against your expectation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Names&lt;/strong&gt;: is everything you expected present — and are you not about to grab a similarly-named variant or an older version?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State and size&lt;/strong&gt;: an asset that's mid-processing or zero bytes isn't distributable, no matter how correct its name is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt;: if one release serves several purposes, filter to your own files by name pattern instead of counting totals, so unrelated uploads don't move your baseline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then record the listing. This matters more than it looks: the asset list is evidence of &lt;em&gt;what the server claims to hold&lt;/em&gt;. Evidence of &lt;em&gt;what a consumer can actually receive&lt;/em&gt; doesn't exist yet — that's the next step, and conflating the two is exactly the mistake the URL already invited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Re-download through the consumer's path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DOWNLOAD_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

gh release download &lt;span class="s2"&gt;"&amp;lt;release-tag&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pattern&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;asset-pattern&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOWNLOAD_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make this step meaningful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty directory.&lt;/strong&gt; If a stray local copy is sitting in the target dir, you can end up hashing a file that never went through the distribution path. &lt;code&gt;mktemp -d&lt;/code&gt; makes the mistake hard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer's path, consumer's permissions.&lt;/strong&gt; Checking the folder you uploaded from proves nothing about whether the distribution channel works. And on a private repo, "an admin can download it" and "the intended consumer can download it" are different claims — test with the access level your consumers actually have instead of widening permissions until the error goes away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the download completes, don't jump to hashing. List what actually arrived and compare it against the recorded asset list first: anything missing, anything extra, any size that doesn't match — stop here.&lt;/p&gt;

&lt;p&gt;This is also where my own avatar-assets check is parked. The log line says &lt;code&gt;download=failure&lt;/code&gt;. Because re-download hasn't passed, everything downstream — size confirmation, hash comparison, round-trip completion — is unverified, and my records say exactly that instead of something more comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Compare SHA-256 across the boundary
&lt;/h2&gt;

&lt;p&gt;Matching name and matching size still don't mean matching bytes. SHA-256 computes a fixed-length digest over the entire content; change any byte and the digest changes completely. It's standardized in &lt;a href="https://csrc.nist.gov/pubs/fips/180-4/upd1/final" rel="noopener noreferrer"&gt;NIST's Secure Hash Standard (FIPS 180-4)&lt;/a&gt;, and &lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/sha2-utilities.html" rel="noopener noreferrer"&gt;GNU coreutils documents the toolchain&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 &lt;span class="s2"&gt;"&amp;lt;canonical-original&amp;gt;"&lt;/span&gt;
shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOWNLOAD_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&amp;lt;asset-name&amp;gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Linux&lt;/span&gt;
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;canonical-original&amp;gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOWNLOAD_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&amp;lt;asset-name&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comparison is only meaningful if the two sides sit on opposite sides of the distribution boundary: the canonical original you fixed in step 1 on one side, the copy that round-tripped through the release on the other. Hashing two copies that live in the same place tells you nothing you didn't know.&lt;/p&gt;

&lt;p&gt;On a mismatch, check &lt;em&gt;what you compared&lt;/em&gt; before suspecting corruption: right file? right version? right release? any transform in between (re-save, re-compress, CDN)? What you don't do is shrug it through because "it looks the same". An unexplained mismatch keeps distribution on hold until it's explained.&lt;/p&gt;

&lt;p&gt;What a match looks like is unspectacular by design — the same digest on both sides of the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;same-64-hex-digest&amp;gt;  &amp;lt;canonical-original&amp;gt;
&amp;lt;same-64-hex-digest&amp;gt;  $DOWNLOAD_DIR/&amp;lt;asset-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any difference between the two lines, however small the underlying change, produces completely different digests.&lt;/p&gt;

&lt;p&gt;If you maintain a manifest of expected digests, &lt;code&gt;sha256sum -c manifest.sha256&lt;/code&gt; runs the whole comparison mechanically and goes red on any difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Record what you verified, not what you planned
&lt;/h2&gt;

&lt;p&gt;The last stage is a ledger: for each asset, the expectation side (role, expected format, release-side name, where the baseline digest lives) and the observation side (name actually present, download path that worked, re-download result, hash result).&lt;/p&gt;

&lt;p&gt;Two rules keep the ledger honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't collapse states into "done".&lt;/strong&gt; Use states that say what was actually checked: &lt;em&gt;planned&lt;/em&gt;, &lt;em&gt;upload confirmed&lt;/em&gt;, &lt;em&gt;re-downloaded&lt;/em&gt;, &lt;em&gt;hash verified&lt;/em&gt;, &lt;em&gt;on hold&lt;/em&gt;. A "planned" URL written before the release existed is fine — as long as it can't be mistaken for a verified one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't overwrite expectation with observation.&lt;/strong&gt; If the plan said one thing and reality said another, that difference is information. Keep both columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record the run conditions too — which release, which pattern, which auth, the fact that the download dir was empty, which baseline version you compared against. The same commands mean different things under different conditions. And keep the ledger itself under version control: a verified state with a lost ledger puts the next check back at zero.&lt;/p&gt;

&lt;p&gt;The completion condition is not "I updated the ledger". It's "the ledger matches the facts I actually verified".&lt;/p&gt;

&lt;h2&gt;
  
  
  When a check goes red, split the failure in two
&lt;/h2&gt;

&lt;p&gt;Automating this verification adds a new failure mode: the verifier itself. A red check does not mean the artifact is broken, and a green check does not mean everything is there.&lt;/p&gt;

&lt;p&gt;Before touching the artifact, check the verifier's wiring: API target, working directory, auth, release tag, glob pattern. Any one of those being stale produces a red that has nothing to do with your files. Conversely, a green that verified one file that happened to exist says nothing about the full set being present.&lt;/p&gt;

&lt;p&gt;Keeping "inventory" (what does the server claim to hold) and "round-trip" (can I get it back and does it match) as separate jobs with separate records is what makes this split cheap: you can see whether the listing failed, the download failed, or the content mismatched, instead of staring at one merged red light.&lt;/p&gt;

&lt;p&gt;And on the red path, hold the line on two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never swap targets mid-verification.&lt;/strong&gt; Switching to a different file or release to make the check pass replaces the question you were answering. The result may look green; it verified something else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume conditions are concrete.&lt;/strong&gt; "The error went away" doesn't reopen distribution. The listing matches expectation again, the same target re-downloads into a fresh empty directory, and the hash comparison passes — that reopens it. If you fixed the verifier, redo the artifact checks from scratch: the verifier working again and the artifact being correct are separate facts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This applies directly if an AI agent or a pipeline does the uploading for you. "Upload this" invites a report of &lt;em&gt;accepted&lt;/em&gt; as if it were &lt;em&gt;delivered&lt;/em&gt;. Ask for the asset list, the re-download result, the hash comparison, and the ledger update as separate results, and you can see exactly which stage stopped — the same property this whole procedure is built around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where my own check is stuck right now
&lt;/h2&gt;

&lt;p&gt;Full disclosure of the current state, because the honest version is more useful than a tidy one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The article-images release works — it's load-bearing for my published articles daily, which is a continuous, if informal, round-trip test.&lt;/li&gt;
&lt;li&gt;The avatar-assets release is parked at step 3. The manifest lists the files to distribute and where their SHA-256 baselines live; the automation log records &lt;code&gt;download=failure&lt;/code&gt;; therefore size confirmation, hash comparison, and round-trip completion are all &lt;em&gt;unverified&lt;/em&gt; and recorded as such.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing in my ledger claims those files are recoverable, because nothing has proven it yet. That's not the procedure failing. That's the procedure doing the one thing it's for: refusing to let "the URL works" stand in for "I got the bytes back".&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist
&lt;/h2&gt;

&lt;p&gt;If a GitHub Release (or any artifact store) holds files you'll need back someday, run this before calling it done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write down which file, in which role, in which format you expect to recover — before looking at what's there.&lt;/li&gt;
&lt;li&gt;List the release's assets via the API and record &lt;code&gt;name&lt;/code&gt; / &lt;code&gt;state&lt;/code&gt; / &lt;code&gt;size&lt;/code&gt; against that expectation.&lt;/li&gt;
&lt;li&gt;Re-download into an empty directory, through the path and permissions your consumers actually use.&lt;/li&gt;
&lt;li&gt;Compare SHA-256 digests across the distribution boundary — canonical original vs round-tripped copy.&lt;/li&gt;
&lt;li&gt;Record per-asset states the ledger can defend: planned / upload confirmed / re-downloaded / hash verified / on hold.&lt;/li&gt;
&lt;li&gt;On any red, split verifier failure from artifact failure before touching either.&lt;/li&gt;
&lt;li&gt;Never swap the target mid-check, and treat "error went away" as insufficient to resume.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Isn't downloading from the release page by hand enough?
&lt;/h3&gt;

&lt;p&gt;For everyday use, often yes. As verification that a delivery or a canonical archive is intact, no — you still haven't checked that the &lt;em&gt;set&lt;/em&gt; is complete or that the &lt;em&gt;content&lt;/em&gt; matches your baseline. Right version, no missing files, matching digest: three separate checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  If the size matches, is it the same file?
&lt;/h3&gt;

&lt;p&gt;No. Size is a cheap early filter for empty files and gross truncation. Different content with identical size is entirely possible; when content identity matters, compare hashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  If SHA-256 matches, am I done?
&lt;/h3&gt;

&lt;p&gt;For that one file's content, against the copy you compared — yes, that's a strong result. Whether &lt;em&gt;all&lt;/em&gt; required files are present, and whether the download path will still be reachable next time, are separate questions. That's what the asset list and the ledger cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  There's a failure in my logs. Can I just retry until it's green?
&lt;/h3&gt;

&lt;p&gt;Retry, sure — but don't reinterpret. The check stays parked at the failed stage, the failure and its conditions go in the ledger, and the retry targets the &lt;em&gt;same&lt;/em&gt; file and release. Quietly retrying against a different target destroys the comparison you were making.&lt;/p&gt;

&lt;h3&gt;
  
  
  I already keep a manifest. Doesn't that cover this?
&lt;/h3&gt;

&lt;p&gt;A manifest fixes your expectation, which is genuinely half the work. It cannot rule out that a file was deleted, replaced, or became unreachable after you wrote it. The manifest is the expectation side; the asset list, the re-downloaded files, and the hash results are the observation side. You need both — and the release's own asset list doesn't replace the manifest either, because it can't tell you which original, in which role, each asset is supposed to correspond to.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>cicd</category>
      <category>bash</category>
    </item>
  </channel>
</rss>
