<?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: Alkis Yuv</title>
    <description>The latest articles on DEV Community by Alkis Yuv (@alkisyuv).</description>
    <link>https://dev.to/alkisyuv</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%2F2613255%2F63a41869-a833-4d3b-9fe2-bf85cdf50ed9.png</url>
      <title>DEV Community: Alkis Yuv</title>
      <link>https://dev.to/alkisyuv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alkisyuv"/>
    <language>en</language>
    <item>
      <title>What my agents do while I sleep</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:59:19 +0000</pubDate>
      <link>https://dev.to/alkisyuv/what-my-agents-do-while-i-sleep-1bkg</link>
      <guid>https://dev.to/alkisyuv/what-my-agents-do-while-i-sleep-1bkg</guid>
      <description>&lt;p&gt;Every night a scheduled job wakes up, reads &lt;a href="https://dev.yuv.run/writing/the-queue/" rel="noopener noreferrer"&gt;a queue of pending work&lt;/a&gt; across my projects, and spawns one headless agent session per target to drain it. In the morning I read a report over coffee. That's the pitch, and scheduling it is genuinely the easy part, any cron line can do that. What took actual design is the policy around it, because an unattended agent with write access is a very fast way to wake up to a mess. Five laws came out of that design.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The runner owns the envelope, the child owns the work
&lt;/h2&gt;

&lt;p&gt;A headless session cannot mind its manners, it won't maintain a lock, it won't notice a sibling, it dies without cleaning up. So the runner does all of that on its behalf. It takes &lt;a href="https://dev.yuv.run/writing/claims-and-locks/" rel="noopener noreferrer"&gt;the claim&lt;/a&gt; on the target before spawning the child, refreshes the heartbeat on a timer while the child runs, and releases it always, crash included. Manners stop being the child's job, which means they stop being a hope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runner: take the claim on the target
  │
  ├─▶ spawn the child, headless, in its own worktree
  │       │  heartbeat renewed on a timer while it runs
  │       ▼
  │   child commits, or finds nothing, or dies, or hits the watchdog
  │       │
  └───────┴─▶ release the claim, always: exit, crash and kill alike
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Skip, never fight
&lt;/h2&gt;

&lt;p&gt;Five conditions make the runner walk away from a target, every one with a logged reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a live interactive session on it&lt;/li&gt;
&lt;li&gt;a dirty tree&lt;/li&gt;
&lt;li&gt;someone else's claim&lt;/li&gt;
&lt;li&gt;an entry marked as needing me&lt;/li&gt;
&lt;li&gt;two failed attempts already tonight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The night never contends with the day. Contention is what mornings are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Nothing touches main
&lt;/h2&gt;

&lt;p&gt;Children commit to their own trees, and where pushing is enabled at all, the runner pushes the night's work to an isolated per-night branch and restores whatever branch was checked out before. I review drained work the way I'd review a colleague's branch, not by discovering it merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ceilings, set before the first run
&lt;/h2&gt;

&lt;p&gt;Underneath it all, ceilings. A cap on children per night, a cap on retry rounds, a watchdog per child. I set them before the first unattended run, not after the first runaway, which I recommend as an ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The report is unconditional
&lt;/h2&gt;

&lt;p&gt;The rule I'd defend hardest. A report gets written even when there was nothing to do. This sounds trivial and it changes everything, because it converts silence into signal. An absent report can only mean the job died. Before this rule, a quiet morning meant either a quiet night or a broken scheduler, and I had to go check which.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Automation you have to check up on is just unfinished automation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The morning read is the acceptance test for the whole thing. The report's skeleton is three headings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Night report 2026-08-14&lt;/span&gt;
&lt;span class="gu"&gt;## what happened&lt;/span&gt;
&lt;span class="gu"&gt;## what was skipped, and why&lt;/span&gt;
&lt;span class="gu"&gt;## what needs me&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it can't fill those inside two minutes of my attention, the report is the bug. Five nightly reports in, the streak is young but every morning has had one waiting, and that is the property I refuse to lose. The coffee helps.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A session that closes has to prove it cleaned up</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:58:43 +0000</pubDate>
      <link>https://dev.to/alkisyuv/a-session-that-closes-has-to-prove-it-cleaned-up-cij</link>
      <guid>https://dev.to/alkisyuv/a-session-that-closes-has-to-prove-it-cleaned-up-cij</guid>
      <description>&lt;p&gt;The process monitor said the audio daemon was at 340% CPU. That was the whole symptom, hours after the session that caused it had closed, on a machine that was otherwise fine. Underneath: 326 audio capture streams running at once, 37 GB of raw audio in a temp directory, and a maintenance loop in my dictation setup that had leaked one recorder process per minute since a session earlier that day. That session had ended normally, reported nothing, and left the damage behind. Nothing I had built had a step where a session answers for the machine it is leaving.&lt;/p&gt;

&lt;p&gt;Sessions close all the time without a goodbye. A context window ends, the pane gets killed, the laptop sleeps, and whatever the session spawned either keeps running or stops, and either can be wrong. &lt;a href="https://dev.yuv.run/writing/claims-and-locks/" rel="noopener noreferrer"&gt;The locks&lt;/a&gt; other sessions must respect, the throwaway worktrees the night run cuts, recorder processes, uncommitted files: all of it is state that outlives the session, and no later session knows to look at it. It is the same failure as &lt;a href="https://dev.yuv.run/writing/night-shift/" rel="noopener noreferrer"&gt;the report nobody reads&lt;/a&gt;, in a different costume.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A rule no mechanism forces through your eyes is prose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the mechanism for the end of a session.&lt;/p&gt;

&lt;p&gt;The design is one script and two hooks, and the property I was after lives in the second hook: a skipped gate cannot stay skipped.&lt;/p&gt;

&lt;p&gt;Session end runs a suite of checks and writes a marker file for the project: green with a timestamp, or debt with the findings. Session start reads the marker. Green and less than a week old means silence; nothing printed, the session begins. Missing, stale or red means the previous session skipped the gate or failed it, and the new session inherits the findings as injected context (a start hook's output is context, that is the documented contract) and has to clear them and run the end check itself before doing anything else. A session that dies mid-flight leaves no marker, so the next one runs the suite on its behalf. There is no path through which the debt goes unseen, which is the only property that matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session end ──▶ run the suite ──▶ marker: ok, or debt + findings
                                     │
session start ──▶ read the marker ◀──┘
   ├─ ok, under a week old ──▶ silence, the session begins
   └─ missing, stale or debt ──▶ findings injected as context;
                                 clear them, run "end", then work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite is deliberately small, and every check names a real incident. Capture streams above two: the 326-stream class. The temp directory over 512 MB or twenty files: the 37 GB class. A lock older than forty-eight hours: a session that died holding it. Uncommitted paths at session end: the wrap-up rule, previously a habit, now checkable. A sweep worktree older than five days: a crashed night. Here is a runnable version with the first three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# session-gate.sh end|start&lt;/span&gt;
&lt;span class="nv"&gt;MARK&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;/.agent-gate/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;    &lt;span class="c"&gt;# (1)&lt;/span&gt;
&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
note&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;  - &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
suite&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                                           &lt;span class="c"&gt;# (2)&lt;/span&gt;
  &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; avfoundation | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; note &lt;span class="s2"&gt;"AUDIO: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; capture streams (expect 1)"&lt;/span&gt;
  &lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sk&lt;/span&gt; /tmp/capture-live 2&amp;gt;/dev/null | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&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;kb&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 512000 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; note &lt;span class="s2"&gt;"TMP: capture-live &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;KB"&lt;/span&gt;
  &lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&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;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; note &lt;span class="s2"&gt;"REPO: &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; uncommitted paths"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
mark&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                                            &lt;span class="c"&gt;# (3)&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MARK&lt;/span&gt;&lt;span class="p"&gt;%/*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debt
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"state":"%s","when":"%s","findings":"%s"}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&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;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;end&lt;span class="p"&gt;)&lt;/span&gt;   suite&lt;span class="p"&gt;;&lt;/span&gt; mark &lt;span class="p"&gt;;;&lt;/span&gt;
  start&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; suite&lt;span class="p"&gt;;&lt;/span&gt; mark&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;         &lt;span class="c"&gt;# (4)&lt;/span&gt;
         &lt;span class="nv"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-7&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;            &lt;span class="c"&gt;# (5)&lt;/span&gt;
         &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"state":"ok"'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fresh&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
         &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SESSION-GATE DEBT: the previous session left work."&lt;/span&gt;
         &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Clear it, then run: session-gate.sh end"&lt;/span&gt;
         &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/.*"findings":"\(.*\)".*/\1/p'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; |
           &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\\n/\n/g'&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;                      &lt;span class="c"&gt;# (6)&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The marker is per project, so debt greets the session that comes back to the place it was made, not a stranger in an unrelated repo.&lt;/li&gt;
&lt;li&gt;One line per check, each named after a real incident; the suite grows by postmortem, never by imagination.&lt;/li&gt;
&lt;li&gt;Written on every end, green or red, so an absent marker can only mean a session that never reached its end.&lt;/li&gt;
&lt;li&gt;A start with no marker runs the suite itself, on the dead session's behalf.&lt;/li&gt;
&lt;li&gt;Green expires after a week; an old green is not evidence.&lt;/li&gt;
&lt;li&gt;The findings print verbatim, and a start hook's output is injected context, so the debt is the first thing the new session reads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wire &lt;code&gt;end&lt;/code&gt; to your agent's session-end hook and &lt;code&gt;start&lt;/code&gt; to its session-start hook, and it runs on every session from then on. Had it existed that day, the next session would have opened on this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;session-gate.sh start
SESSION-GATE DEBT: the previous session left work.
Clear it, &lt;span class="k"&gt;then &lt;/span&gt;run: session-gate.sh end
  - AUDIO: 326 capture streams &lt;span class="o"&gt;(&lt;/span&gt;expect 1&lt;span class="o"&gt;)&lt;/span&gt;
  - TMP: capture-live 38797312KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing about the shape. The checks are fail-soft and fast; the gate is not there to stop a session from ending, it is there to make the next session's first screen tell the truth.&lt;/p&gt;

&lt;p&gt;What I would generalise: any resource a session can leave behind needs a counter somewhere, and the session boundary is the cheapest place to read it. The count that told the truth that day was capture streams, 326 of them, and that count now runs at every session end, whether or not the session remembers to.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>The runner deleted what it told me to rescue</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:58:07 +0000</pubDate>
      <link>https://dev.to/alkisyuv/the-runner-deleted-what-it-told-me-to-rescue-5g96</link>
      <guid>https://dev.to/alkisyuv/the-runner-deleted-what-it-told-me-to-rescue-5g96</guid>
      <description>&lt;p&gt;One morning my night report said the same thing about two projects, six times over:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- work is committed but the push failed, manual push needed&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I went looking for the work. There wasn't any. The children had died in the first second of their lives, a harness binary had moved the day before and was no longer on the path, and the runner had reported a push failure for sessions that never started. The next night the children ran properly, committed real work in four projects, and the runner deleted all of it. Same message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.yuv.run/writing/while-i-sleep/" rel="noopener noreferrer"&gt;The night job&lt;/a&gt; had just gained a merge-request flow. Each child works in a throwaway worktree on a per-night branch; when it commits, the runner pushes the branch and opens a merge request for my morning. Two defects were stacked under that one sentence, and the second is the one worth the essay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;child, in a throwaway worktree   runner
  commits on the night branch
           │
           ▼
                                 did it commit? compare with upstream
                                 ✗ fresh branch, no upstream: every
                                   child reads as "has unpushed work"
                                 push, from the worktree
                                 ✗ push URL disabled there, by design
                                 report: push failed, manual push needed
                                 cleanup: remove worktree, delete branch
                                 ✗ the only ref to the commits is gone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first was the test for "did the child commit anything". It compared the branch against its upstream. A fresh branch has no upstream, the lookup failed, the fallback value read as "has unpushed work", and from then on every child, working or dead, looked identical to the runner. A proxy for the fact, not the fact.&lt;/p&gt;

&lt;p&gt;The second was the push itself. My worktrees have their push URL disabled by design, it is the rail that guarantees an unattended agent can never push, so the push ran from exactly the place that cannot push. It failed every night it was tried. The report said manual push needed. And then the cleanup step, which had never been told about any of this, removed the worktree and deleted the branch, the only reference to the night's commits. The runner destroyed the thing it had just asked me to save, and the morning report, which files a push failure under things I fix by hand after coffee, gave no sign there was nothing left to push.&lt;/p&gt;

&lt;p&gt;What saved the work is git's habit of keeping unreferenced objects around until a garbage collection. The rescue is three commands, and the comment is the important one:&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;git fsck &lt;span class="nt"&gt;--no-reflogs&lt;/span&gt; &lt;span class="nt"&gt;--lost-found&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;commit
dangling commit 3f9c2a1d7e...
dangling commit 8b07d4c5a2...
&lt;span class="nv"&gt;$ &lt;/span&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; 3f9c2a1
3f9c2a1 drain: align the retry helper with the new &lt;span class="nb"&gt;timeout &lt;/span&gt;API
&lt;span class="nv"&gt;$ &lt;/span&gt;git branch rescue/2026-08-21 3f9c2a1      &lt;span class="c"&gt;# a ref, nothing else&lt;/span&gt;
&lt;span class="c"&gt;# do NOT gc, prune or worktree-prune first: unreachable is what gc&lt;/span&gt;
&lt;span class="c"&gt;# deletes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four repos came back that way the same night. A fifth had two orphans the sweep missed, found by hand while writing this; one was five days old, and a later merge message had described it as already on main.&lt;/p&gt;

&lt;p&gt;The fix is three changes, and each is a version of the same rule: decide from what verifiably exists, never from what a message says.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-# did the child commit? a proxy: compare the branch with its upstream
-n=$(git rev-list --count "$b@{u}..$b" 2&amp;gt;/dev/null || echo 1)
&lt;/span&gt;&lt;span class="gi"&gt;+# did the child commit? the fact: HEAD moved while the child ran
+pre=$(git rev-parse HEAD)
+run_child
+post=$(git rev-parse HEAD)
+[ "$post" != "$pre" ] &amp;amp;&amp;amp; committed=1
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-# cleanup: always
-git worktree remove "$wt" &amp;amp;&amp;amp; git branch -D "$b"
&lt;/span&gt;&lt;span class="gi"&gt;+# cleanup: only when the remote verifiably holds every commit
+n=$(git rev-list --count "$b@{u}..$b" 2&amp;gt;/dev/null || echo 1)
+[ "$n" -eq 0 ] &amp;amp;&amp;amp; git worktree remove "$wt" &amp;amp;&amp;amp; git branch -D "$b"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outcome is measured as HEAD movement during the child's run. A child that dies within thirty seconds with no commits is reported as exactly that, with the last two lines of its own output quoted in the report, and it gets no more rounds that night; a real session takes minutes even when it finds nothing to do. The push goes from the parent checkout, where pushing is allowed. And the branch is deleted only when the count of commits the remote lacks is zero, or the commits never existed. "Push failed" now means a commit exists and the remote does not have it, which is the only thing it ever should have meant.&lt;/p&gt;

&lt;p&gt;The lesson generalises past git. Every night produces a report, and I had been treating the report as the state. It is a claim about the state, made by the same program that made the mistake.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- The destructive step in any automation has to be conditioned on the verified success, not the reported one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the two failures that look alike from outside, never started and started-then-failed, need different words, because the operator does different things for each. I had &lt;a href="https://dev.yuv.run/writing/rules-that-survive/" rel="noopener noreferrer"&gt;written that rule down&lt;/a&gt; for agents: re-verify before you repeat, assert from the primary source. It applies to shell scripts just as well, and they are even less likely to feel the tell.&lt;/p&gt;

</description>
      <category>git</category>
      <category>automation</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>One directory is the task manager my agents share</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:57:31 +0000</pubDate>
      <link>https://dev.to/alkisyuv/one-directory-is-the-task-manager-my-agents-share-4ecc</link>
      <guid>https://dev.to/alkisyuv/one-directory-is-the-task-manager-my-agents-share-4ecc</guid>
      <description>&lt;p&gt;When you run agents across many projects, findings stop respecting project boundaries almost immediately. A session working on one tool discovers a bug in another. A review in one repo produces work for three. My first instinct was the obvious one, let the session go fix it over there. That instinct is how two agents end up editing the same tree, and after &lt;a href="https://dev.yuv.run/writing/claims-and-locks/" rel="noopener noreferrer"&gt;auditing five collisions&lt;/a&gt; I banned it. A session's only sanctioned write into another project is a queue entry.&lt;/p&gt;

&lt;p&gt;The queue is embarrassingly low-tech, one directory per target, one markdown file per task, a status line in the front matter: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;taken&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;dropped&lt;/code&gt;. No database, no integration, and the only board is a one-page summary generated from the files themselves. Files stay put forever as history, and the receiving project's next session gets offered its pending entries automatically when it starts, take, defer, or drop, and the answer is written into the entry so nothing ever asks twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handovers/
├── board.md                  generated from the entries, one page
├── site/
│   ├── 004-syndication.md    status: done
│   └── 007-display-shapes.md status: pending
└── tooling/
    └── 012-retry-helper.md   status: taken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lifecycle is four states and two side doors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              take                       evidence pasted
 pending ──────────────▶ taken ──────────────────────────▶ done
   │  ▲                    │
   │  │ date reached       └──▶ dropped, reason written into the entry
   │  │
   ▼  │
 snoozed, at most a week       severity: risk ignores the snooze and
                               resurfaces every session until dealt with
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete entry fits on one screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;done&lt;/span&gt;
&lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;normal&lt;/span&gt;
&lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-08-09&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# Align the retry helper with the new timeout API&lt;/span&gt;

The ask: the helper still passes an option the API dropped in v3.
Update the call sites and run the suite.
Context: the failing CI run, and the changelog entry that dropped it.
Acceptance: &lt;span class="sb"&gt;`npm test`&lt;/span&gt; exits 0 with the retry cases green.
Evidence: "12 passed, 0 failed", pasted by the taker, 2026-08-10.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes it work is not the format though, it's three authoring rules that came from watching it fail.&lt;/p&gt;

&lt;p&gt;Every entry carries an acceptance test. The body is written so the receiving session needs nothing else, the ask, the context links, and how to know it's done. Entries that skipped this read like riddles a week later, and riddles get dropped.&lt;/p&gt;

&lt;p&gt;Done requires evidence. One pasted line showing the acceptance test passing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- An assertion without evidence is not done, it's a hope with a status field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This rule came directly from catching claims of finished work that a thirty-second check would have disproven.&lt;/p&gt;

&lt;p&gt;And absence claims name what was checked. An entry once justified itself with "no record says this value is deliberate" while the record said exactly that, one file over. I ruled on a false premise that day. Since then, "nothing documents X" is only writable alongside the list of places you looked.&lt;/p&gt;

&lt;p&gt;There is also a small vocabulary for time and urgency, a &lt;code&gt;snooze&lt;/code&gt; field that hides an entry until a date, hard-capped at a week because my setup changes too fast for longer parking, and a &lt;code&gt;risk&lt;/code&gt; severity that ignores snoozing entirely and resurfaces every session until someone deals with it.&lt;/p&gt;

&lt;p&gt;The part I like most, the same queue serves humans and machines. &lt;a href="https://dev.yuv.run/writing/while-i-sleep/" rel="noopener noreferrer"&gt;My nightly automation&lt;/a&gt; drains the same entries my interactive sessions do, skips the ones marked as needing me, and flips the same statuses with the same evidence rule. One protocol, no translation layer. For coordination between agents, I keep finding that a directory of honest text files beats anything cleverer I've tried.&lt;/p&gt;

&lt;p&gt;The queue's best story is not in this piece though. It's &lt;a href="https://dev.yuv.run/writing/night-shift/" rel="noopener noreferrer"&gt;the night my agents built the same feature twice&lt;/a&gt;, where every file told the truth and I was the part that didn't.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Rules my agents actually follow</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:56:55 +0000</pubDate>
      <link>https://dev.to/alkisyuv/rules-my-agents-actually-follow-24ed</link>
      <guid>https://dev.to/alkisyuv/rules-my-agents-actually-follow-24ed</guid>
      <description>&lt;p&gt;I once measured how long a written rule survives a fresh agent session: 67 minutes. The rule was recorded right after one session swept another session's half-finished files into its commit, and it was plain:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Stage explicit paths only, never &lt;code&gt;git add -A&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next session had never lived the incident, read the rule as advice, and did the thing anyway.&lt;/p&gt;

&lt;p&gt;Everyone who works with coding agents ends up keeping some kind of mistakes file, the agent breaks something, you write down what happened, and you hope the note prevents a repeat. I kept one too. What the 67 minutes taught me is that the file is the easy third of the problem. The other two thirds are getting the rule enforced, and keeping the record small enough that anyone, human or agent, still reads it.&lt;/p&gt;

&lt;p&gt;A written rule does not survive on its own, that is the honest part. Agents repeat mistakes while remembering the rule, you can watch them acknowledge it and do the thing anyway. Prose loses the fight against a model's priors. So my decision records stopped being prose and became entries with a fixed anatomy. This is the whole shape, using that staging rule as the worked example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## D-035 — 2026-08-02 — Stage explicit paths only, never add-all&lt;/span&gt;
&lt;span class="gs"&gt;**Decision:**&lt;/span&gt; one imperative sentence, no hedging.
&lt;span class="gs"&gt;**Why:**&lt;/span&gt; written for a stranger with no transcript.
&lt;span class="gs"&gt;**Invariant:**&lt;/span&gt; the line that must stay true for the rule to hold.
&lt;span class="gs"&gt;**Boundary:**&lt;/span&gt; the condition under which this rule stops applying.
&lt;span class="gs"&gt;**Rung:**&lt;/span&gt; law or practice, how hard it binds.
&lt;span class="gs"&gt;**Verify:**&lt;/span&gt; &lt;span class="sb"&gt;`grep -rn "git add -A" hooks/ scripts/`&lt;/span&gt; → nothing
&lt;span class="gs"&gt;**Scope:**&lt;/span&gt; which projects the entry covers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two fields that changed everything are the least glamorous ones.&lt;/p&gt;

&lt;p&gt;The first is &lt;em&gt;Why&lt;/em&gt;, written for a stranger with no transcript. Not "as discussed", not a summary of a chat that no longer exists. If the reason can't stand alone, the decision will be relitigated by the next session and probably lost.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;Verify&lt;/em&gt;, a runnable command with an expected output. A rule you can only read is checked by luck. A rule you can execute is checked by machines, on schedule, forever. When one of my entries drifted from reality, the audit flagged it as a failing check and the fix was a one-line commit. Decay surfaced as a finding instead of a surprise. The auditor is the one piece of this system I made public, &lt;a href="https://github.com/triartleet/etymd" rel="noopener noreferrer"&gt;etymd&lt;/a&gt;, it reads the claims your instruction files make and checks them against the actual repo, &lt;code&gt;npx etymd audit&lt;/code&gt; and you have it.&lt;/p&gt;

&lt;p&gt;Between the mistakes file and the record there is a path, and I'd argue this path is the actual system. An incident gets noted where it happened, cheap and unstructured. Recurrence makes it countable. Only a countable pattern graduates into the record, with an invariant and a verify line, and only recorded rules get wired into hooks that block instead of advise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;incident, noted where it happened      cheap, unstructured
        │  happens again
        ▼
countable pattern                      a count, not a feeling
        │  graduates
        ▼
record entry: invariant + verify       law
        │  wired into
        ▼
hook that blocks instead of advising   enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip the middle and you get rules nobody believes. Skip the end and you get beliefs nobody follows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Evidence, then law, then enforcement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then there is bloat, which in the agent era is the default outcome. Models write more than anyone reads, records grow faster than they bind, and a 3000-word decision document is just prose with a fancier name. My counterweights are all mechanical. Index lines have a character budget and a hook rejects the commit that exceeds it, I have watched it reject my own agent mid-task, which is exactly the point. Files have word budgets checked by an audit. A fact lives in exactly one document and the same decision recorded twice is treated as a defect, because two copies always drift.&lt;/p&gt;

&lt;p&gt;The anatomy itself is the quiet anti-bloat device though. Seven short fields leave an essay nowhere to hide.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>git</category>
      <category>bestpractices</category>
    </item>
    <item>
      <title>My night shift built the feature, and my day shift didn't notice</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:56:19 +0000</pubDate>
      <link>https://dev.to/alkisyuv/my-night-shift-built-the-feature-and-my-day-shift-didnt-notice-2p0l</link>
      <guid>https://dev.to/alkisyuv/my-night-shift-built-the-feature-and-my-day-shift-didnt-notice-2p0l</guid>
      <description>&lt;p&gt;I have &lt;a href="https://dev.yuv.run/writing/the-queue/" rel="noopener noreferrer"&gt;a queue where cross-project work waits&lt;/a&gt;, and &lt;a href="https://dev.yuv.run/writing/while-i-sleep/" rel="noopener noreferrer"&gt;a nightly job that drains it&lt;/a&gt;, one headless agent per project, rails everywhere, rails being the mechanically enforced guardrails that stand in for trust in these setups. Last week an entry sat in that queue: build a writing section for my site. What happened to it over one night taught me more about running agents than the feature itself.&lt;/p&gt;

&lt;p&gt;The night run took the entry properly, flipped its status, built the whole thing on an isolated branch, recorded its design decisions, and then stopped one step short on purpose. Its rails forbid publishing to the live site unattended. It wrote, in its own report, that deploying would bypass the morning review, and left me a one-line instruction for the morning. So far, the system working exactly as designed.&lt;/p&gt;

&lt;p&gt;The morning shift was me, working with an interactive agent. And we built the same feature again, from scratch, on the main branch, and deployed it. Not because anything malfunctioned, but because I had read that queue entry a day earlier, when it still said pending, and I carried that stale picture in my head instead of re-reading the entry it would have taken ten seconds to open. The status file said taken. I never looked. And the night's report, the one carrying that one-line instruction, went unacted on that same morning, which I have to own too. A report no gate forces through your eyes is prose as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           night shift, headless         day shift, me with an agent
 evening   takes the entry: taken
           builds on its own branch
           stops before deploy, by rule
           reports: deploy after review
 morning                                 reads my memory: still pending
                                         rebuilds on main, deploys
 later     ── the two builds meet in review; the merge takes both ──
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what I find worth writing down. Every mechanized rail held. The claims kept the two builds from ever touching the same files, the drain's no-publish rule kept the night work reviewable, the branch isolation kept main clean. The only thing that failed was the part that ran on memory instead of mechanism, my own assumption that yesterday's state was still today's.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Stored state decays, and the feeling of already knowing is exactly the moment the ten-second check is worth it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have that rule written down, of course. Rules in prose decay too, that's the joke, and it lands on me this time. So the fix that outlived the joke is mechanical, as it had to be. The queue now generates &lt;strong&gt;a one-page board&lt;/strong&gt; from the entries, and taken sits as its own section on it, the night's claims in front of the day shift's eyes before any work starts. Built before that day ended, naturally.&lt;/p&gt;

&lt;p&gt;The reconciliation had its own lesson. Two implementations of one feature, one by an unattended agent, one by an agent with me in the loop, and when I reviewed them side by side the night version was better. Cleaner architecture, a sitemap that maintains itself, fewer special cases in the server config. It also missed something only the live deployment surfaced, which the day version had already hit and solved. So the merge took the night's architecture and the day's hard-won fix, and both builds ended up mattering, which is a kinder ending than the duplicated hours deserved.&lt;/p&gt;

&lt;p&gt;If you run agents overnight, my takeaways are three. Give the night shift rails, not trust, it will happily respect rules that are mechanically enforced. Make the morning review a real step, not a formality, mine genuinely picked a winner. And before you start any work, re-read the queue, not your memory of the queue. The entry knows. You only think you do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>git</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Everything you ever committed is the product</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:55:44 +0000</pubDate>
      <link>https://dev.to/alkisyuv/everything-you-ever-committed-is-the-product-3pao</link>
      <guid>https://dev.to/alkisyuv/everything-you-ever-committed-is-the-product-3pao</guid>
      <description>&lt;p&gt;I audited three of my repos before making things public. The audit was after one class of thing, anything that tied the code to the private world behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;absolute paths from my machine&lt;/li&gt;
&lt;li&gt;the names of projects that were never meant to be seen&lt;/li&gt;
&lt;li&gt;internal addresses&lt;/li&gt;
&lt;li&gt;the real names of people and places around the work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One test for every line: would it be safe, read by a stranger who knows nothing about me, my machine, or my other projects.&lt;/p&gt;

&lt;p&gt;The audit ran five rounds, a round being one full pass with one scope, because every round found something new. Not because the searching was sloppy. Because each round searched only where it happened to look, and each fix pointed at the next place. Round one read the tracked files and removed a set of strings. Those same strings still sat in every old commit, so git history became round two. Round two rewrote the history, but the packages already built from it still carried the strings, so the published artifacts became round three. The leaks mapped onto blind spots, not onto effort, and that observation ended up shaping the whole system.&lt;/p&gt;

&lt;p&gt;The worst find was not in git at all. A cache file, properly gitignored, carrying an absolute path from my machine, shipped inside three published releases of an extension, because the packager does not read gitignore. Every git-based check passed, forever, while the file went out to two marketplaces. I am deliberately not naming the extension, old releases tend to stay fetchable long after a fix ships, and that is exactly the point of this piece. There is no repo-scoped check that can catch that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Scope, not thoroughness, is the thing to get right.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the setup I run now has four doors, one per exit, each one an ordinary git or packaging hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;working tree ─[1][2]─▶ history ──[3]──▶ remote ──[4]──▶ marketplace
              commit            push           publish

[1] staged content    pre-commit    the lines about to enter history
[2] commit message    commit-msg    messages are history too
[3] full tracked tree pre-push      what a rebase or --no-verify let in
[4] built artifact    package step  the only door the cache file met
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Staged content is screened at commit, a pre-commit hook running the pattern list, absolute paths from your home directory being the classic first entry. The commit message is screened separately by its own hook, messages are published history too and the staged check structurally cannot see them. The full tracked tree is screened at push, a pre-push pass over everything tracked, which catches whatever a rebase or a no-verify commit smuggled in. And the built artifact is screened at publish, wired into the package step itself, the only door that would have caught the marketplace leak, sitting at the irreversible moment on purpose. A commit can be amended. A published package cannot be recalled from the machines that already fetched it.&lt;/p&gt;

&lt;p&gt;Two rules keep the doors honest. Every new class of finding &lt;a href="https://dev.yuv.run/writing/rules-that-survive/" rel="noopener noreferrer"&gt;becomes a rule&lt;/a&gt;, a pattern if it greps, a checklist line if it is a place nobody had inspected. The audit found something new five rounds in a row precisely because each round's lesson used to die with its fix. And when there is a choice, remove the surface instead of adding a rule. The single biggest win of the whole audit was an ignore file that cut a published package from thirteen files to five. Content that never ships cannot leak, and no pattern list will ever be as reliable as absence.&lt;/p&gt;

&lt;p&gt;One more, because it stings. Patterns only catch what you already thought of. A sentence can disclose a fact about your employer while containing no banned string at all. For that class there is no grep, only a periodic fresh read of everything, asking one question, what does this repo reveal about its author. I treat that read as part of releasing, the same as the version bump.&lt;/p&gt;

&lt;p&gt;If you take one thing, take the framing. Publishing is not showing your current code. It is showing every decision, path, and careless message you ever committed, to everyone, forever. Build the doors before the first push, it is genuinely miserable to retrofit.&lt;/p&gt;

</description>
      <category>git</category>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>My agents kept overwriting each other, so I gave them claims</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:55:08 +0000</pubDate>
      <link>https://dev.to/alkisyuv/my-agents-kept-overwriting-each-other-so-i-gave-them-claims-2h8k</link>
      <guid>https://dev.to/alkisyuv/my-agents-kept-overwriting-each-other-so-i-gave-them-claims-2h8k</guid>
      <description>&lt;p&gt;I run several agent sessions in parallel, each in its own project, all on the same machine. For a while I believed that was safe because every session had its own repo. It wasn't. Sessions reach outside their home tree more often than you think, a memory file here, a shared config there, a quick fix in a sibling project because the finding happened to surface elsewhere.&lt;/p&gt;

&lt;p&gt;The collisions, five of them when I sat down and audited the record, all had the same shape. The second session was never blind. It saw the signs of the first one working, a fresh claim in a status file, a half-written directory, and it proceeded anyway. Claims existed then; each project noted them its own way, in its own status file, and nothing read them back. I want to be clear that this is not a model being careless. Under context pressure, an agent treats another agent's presence as noise.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Politeness is not a mechanism, so it doesn't survive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also worth telling, my staging rule failed the same week, broken again 67 minutes after I recorded it, by a session that had never lived the incident. That story owns &lt;a href="https://dev.yuv.run/writing/rules-that-survive/" rel="noopener noreferrer"&gt;its own piece&lt;/a&gt;, because the fix turned out to be nothing like better prose.&lt;/p&gt;

&lt;p&gt;So the claim went from a courtesy to a protocol. Not a new idea, a standardized one: one format, one place, one mechanical refusal, instead of five projects each writing notes nobody was bound to honour. Before any session writes outside its own tree, it takes a claim, a small JSON file named after the target, holding who, when, and what for. The whole file is five fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shared-config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"holder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"session-a41"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"align the lint rule with the new preset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"taken_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-14T21:02:11Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"heartbeat_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-14T21:09:40Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The protocol around it is four verbs. Take before the first foreign write, renew the heartbeat every writing turn, release at the end, and if another session already holds the claim and its heartbeat is fresh, the answer is &lt;code&gt;REFUSED&lt;/code&gt;, and &lt;code&gt;REFUSED&lt;/code&gt; is an answer, not an obstacle. The refused session queues &lt;a href="https://dev.yuv.run/writing/the-queue/" rel="noopener noreferrer"&gt;a handover note&lt;/a&gt; instead, and the work happens later, in order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;foreign write wanted
      │
      ▼
 take &amp;lt;target&amp;gt; ── held, heartbeat fresh? ──yes──▶ REFUSED
      │ no, or stale                                 │
      ▼                                              ▼
 claim written, read back                    queue a handover,
      │                                      the work runs later
      ▼
 beat every writing turn ──▶ release at the end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part I did not expect to matter, and it mattered most, was verifying the guard itself. Early on, the claim script printed "claimed" even when writing the claim file had silently failed (a slash in a target name broke the path). A guard that reports success on failure is worse than no guard, because sessions proceed believing they hold a lock that doesn't exist. The fix was small, slugified names and a checked write, but the lesson was bigger. When you build a safety mechanism for agents, test the mechanism with the same suspicion you had for the agents.&lt;/p&gt;

&lt;p&gt;A healthy machine, at rest, has an empty claims directory. Claims that outlive their session are the first thing I look at when something seems half-written. The whole mechanism is about sixty lines of shell. The value was never the code though. It's the moment this shows up on screen and the wrong move becomes loud instead of silent:&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;claim take shared-config &lt;span class="s2"&gt;"align the lint rule with the new preset"&lt;/span&gt;
REFUSED: shared-config is claimed by session-a41 &lt;span class="o"&gt;(&lt;/span&gt;heartbeat 41s ago&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>I built an AI that writes like me, and I'm measuring how much it fails</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:54:32 +0000</pubDate>
      <link>https://dev.to/alkisyuv/i-built-an-ai-that-writes-like-me-and-im-measuring-how-much-it-fails-4o0</link>
      <guid>https://dev.to/alkisyuv/i-built-an-ai-that-writes-like-me-and-im-measuring-how-much-it-fails-4o0</guid>
      <description>&lt;p&gt;Everyone can now spot machine text on sight, the em-dashes, the "you're absolutely right", the invented technical terms nobody at any job ever used. I write a lot with agents, and I did not want my published words to smell like that. So I built &lt;a href="https://github.com/triartleet/hyphos" rel="noopener noreferrer"&gt;hyphos&lt;/a&gt;, a small open tool that learns how I actually write, from my own words, and rewrites AI drafts to sound like me. This is the build log, including the parts that went wrong.&lt;/p&gt;

&lt;p&gt;The corpus came first. My raw material was every message I've ever typed into my coding agents, plus a decade of sent email. Extracting "what I typed" turned out to be the real work. The first pass over my agent transcripts produced 1.27 million words, which sounded great until I measured where they lived, 91 percent sat in messages too long for anyone to have typed. Pasted logs, quoted docs, other people's text. Nobody types 1500-word prompts. So the filtering is mechanical and merciless, machine-shaped lines stripped out of every message, lengths capped at what a human plausibly types, and any message carrying a machine tell inside supposedly-mine text thrown away whole, 606,000 words in that last cut alone. I am left with 80,470 words that behave like typing, and that number I trust.&lt;/p&gt;

&lt;p&gt;Email had a nastier lesson. I assumed a Sent export contains only my mail. Measured, 45 percent of it was other people's messages, whole threads riding along. If you ever build on personal data, enforce authorship structurally, don't assume it. A From filter and some multilingual quote-stripping later, 84,000 English words that are provably mine, with the Greek kept aside as rhythm signal only. The whole funnel, in numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent transcripts   1,270,000  words extracted
                    91 %       in messages too long to have been typed
                    606,000    thrown away whole for a machine tell
                    80,470     left, and they behave like typing
sent email          45 %       of the export was other people's mail
                    84,000     English words that are provably mine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the part I'd been waiting for, the fingerprints. Per register, because I don't have one voice, the me that instructs an agent is not the me that emails a stranger. Sentence lengths, punctuation habits, casing, openers, contraction rates. One number stopped me though. In my pre-2023 email, written before AI assistance existed, I used an em-dash exactly zero times in thirty thousand words. Not rarely. Never. The internet's favourite AI tell is, in my own measured history, a construction I simply don't make. That single statistic became a hard rule, any em-dash in "my" text gets rewritten out mechanically, no model discretion involved.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- The model handles meaning and register, the rules handle the fingerprint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the design insight I'd defend most, by the way. Style instructions decay, models drift back to their priors within a few replies, everyone has watched it happen. So the quirks that can be enforced deterministically are enforced after the model, by boring code that cannot be argued with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my typing, provably mine ──▶ fingerprint per register
                             (sentence length, commas, casing, openers)
                                      │
AI draft ──▶ model rewrite ──▶ rules: the fingerprint, enforced
             (meaning,         by code (em-dash out, quirks kept)
              register)               │
                                      ▼
                             score: register distance + model-isms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the score. Every output gets a number for how much it statistically resembles my register, plus a count of model-isms found. I'll be honest about where that stands, the model-ism counter works today, the stylometric half is underpowered on short texts and I've marked it low-confidence below 250 words rather than pretend otherwise. The calibration that gives the number meaning is a blind test, the tool shows me snippets, some mine, some machine, and my own failure to tell them apart is the target metric. When I can't beat a coin flip against my own rewrites, the tool works.&lt;/p&gt;

&lt;p&gt;The first round is in, actually. Twelve snippets, six mine, six the engine's, shuffled, key sealed until I answered. I scored six of twelve. Exactly a coin flip. Three engine texts passed as mine, and stranger, I disowned three things I genuinely wrote, one of them with full confidence. What gave the generated ones away, when anything did, was the topic, not the tone, and topics are outside the tool's job on purpose. Small sample, one register, so I'm not framing this as proof. But as a first measurement it is uncomfortably encouraging.&lt;/p&gt;

&lt;p&gt;hyphos is &lt;a href="https://www.npmjs.com/package/hyphos" rel="noopener noreferrer"&gt;open source&lt;/a&gt;, local-first, and your writing never leaves your machine. Whether it convinces you is something you can measure yourself, which is rather the point.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>writing</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>What my agents are allowed to remember</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:53:28 +0000</pubDate>
      <link>https://dev.to/alkisyuv/what-my-agents-are-allowed-to-remember-46fd</link>
      <guid>https://dev.to/alkisyuv/what-my-agents-are-allowed-to-remember-46fd</guid>
      <description>&lt;p&gt;Every agent platform ships memory now. Persistence across sessions went from a trick to a checkbox sometime this year, and I expected that to settle the question. It didn't. My agents remembered plenty. They just never remembered anything useful, and the first time I audited the store I found out why: the memory was filed like a library and read like a headline.&lt;/p&gt;

&lt;p&gt;Here is the mechanism that bit me. A memory store has two layers, an index file that loads into every session, and a body file per memory that loads only if something sends the agent to read it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memory/
├── MEMORY.md            the index: one line per memory, loaded
│                        into every session
├── deploy-cache.md      a body: opened only when its index line
├── editor-profile.md    sends the agent there, which is rarely
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had written the index as titles. "Deploy notes". "Editor profile gotcha". The facts those titles pointed at lived in the bodies, and the bodies never got opened, because nothing in a title tells an agent mid-task that the body matters. The index was the only layer reliably read, and I had filled it with pointers to the layer that wasn't. Memories that cost real incidents to learn sat there, correctly filed, dead.&lt;/p&gt;

&lt;p&gt;The routing rule that fixed it fits in one line: the actionable atom goes into the index line itself, and the body holds only what is worth a deliberate fetch, the why, the full runbook. A dead line and a live one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Deploy notes
&lt;/span&gt;&lt;span class="gi"&gt;+ Deploys need the cache cleared first, or the old bundle serves an hour
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body can explain the hour.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Write every index line as if it is all the agent will ever see, because usually it is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That fixed retrieval. The bigger problem was admission, because an agent that can write memory will write memory about everything. So a memory earns its slot by passing three tests, and fails on any miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not derivable. If the repo, its history or its docs already record it, the memory is a copy, and copies drift. One home per fact.&lt;/li&gt;
&lt;li&gt;Durable. True beyond this conversation. "We are editing the parser today" is context, not memory.&lt;/li&gt;
&lt;li&gt;Behavior-changing. A future session acts differently for having it. Trivia fails; preferences, gotchas and standing rulings pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shape decides where a survivor goes. A rule that fires on the shape of a task, "never stage with add-all", gets a one-line pin in the instructions that load every time. A plain fact gets a self-contained index line. A fact about one project lives in that project's store and nowhere else; the global store is for the machine and the person.&lt;/p&gt;

&lt;p&gt;Then there is decay, which is the part nobody warns you about. A stored claim is a claim about the past. Repos merge, flags flip, another session fixes the thing, and the memory sits there unchanged and confident. I lost a day to this once, &lt;a href="https://dev.yuv.run/writing/night-shift/" rel="noopener noreferrer"&gt;duplicating a feature my night shift had already built&lt;/a&gt;, on the strength of a queue entry I had read the day before and carried in my head. Two rules now handle it. Re-verify on repeat, or drop: any stored claim about to be repeated or acted on gets checked against the primary source, the file over the summary, the machine over the memory, or it gets deleted. And the tell for when to check is the feeling of already knowing. Every stale assertion I have audited in my own sessions had that same signature, a fact held confidently enough that checking felt redundant. That feeling is the trigger, not the excuse. The belief you are about to act on is exactly the one worth the ten-second look.&lt;/p&gt;

&lt;p&gt;Which leaves bloat. Agents inflate everything they write and memory is no exception, so the counterweight is machinery, never advice. My index lines carry a hard character budget and a commit hook rejects the commit that exceeds it; the fix is demoting detail into the body, not arguing with the hook. Wrong memories get deleted, not annotated. A store with archaeology layers is a second archive, not a working surface.&lt;/p&gt;

&lt;p&gt;If you want one thing to do today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your store and grade every entry against the three tests.&lt;/li&gt;
&lt;li&gt;Delete the derivable, the transient and the inert.&lt;/li&gt;
&lt;li&gt;Lift every surviving body's atom into its index line.&lt;/li&gt;
&lt;li&gt;Count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If more than half died, you did it right. The store was carrying the noise floor of everything you ever half-thought, and your agents were paying the reading tax every single session.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Every hole in the gate is signed</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:53:24 +0000</pubDate>
      <link>https://dev.to/alkisyuv/every-hole-in-the-gate-is-signed-3a43</link>
      <guid>https://dev.to/alkisyuv/every-hole-in-the-gate-is-signed-3a43</guid>
      <description>&lt;p&gt;A routine commit in my hub repo tripped the content screen last week on text that had been fine for weeks. &lt;a href="https://dev.yuv.run/writing/everything-you-committed/" rel="noopener noreferrer"&gt;The screen&lt;/a&gt; is the pre-commit door that refuses anything matching my leak patterns, and the text it refused was the kind the hub exists to hold: the names it is allowed to name, the banned terms a standard must quote in order to ban them. Those had exemptions. The exemptions lived in a file that had been there since the screen was written, one regex per line, and a new version of the auditor had started reading that format as "unsigned", reporting it, and then ignoring it. Every exemption in the file had silently stopped applying.&lt;/p&gt;

&lt;p&gt;I want to be precise about why this surfaced at all. It failed in the strict direction: the gate checked more, not less, so it blocked a commit and I noticed. The same change could as easily have made a gate check less, and nothing, no commit, no report, no test, would have told me. That asymmetry is the whole argument of this piece.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A hole in a gate that nobody signed and a gap in a gate that nobody declared are the same object, a thing no one can audit later, and the difference between them is only which way the next upgrade happens to fall.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the exemption format changed, and the change is the signature. Each exemption is now a block of labeled lines, and one missing field means the exemption does not apply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-^AcmeCorp|BetaCorp$
&lt;/span&gt;&lt;span class="gi"&gt;+pattern ^AcmeCorp|BetaCorp$
+reason fixture proving the detector fires on either name
+date 2026-08-15
+author someone
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrating the old file forced the exercise the old format had let me skip: writing the reason next to each pattern. Thirteen bare regexes went in; fifteen signed blocks came out, two of them new, for content the old file had never covered and that only became visible once every block had to say what it was for. One reason field records its own history: the pattern had been a hand-kept roll-call of project names until registering a new project made the hub's own gate refuse the commit that registered it. A list kept by hand lags in the direction that blocks work. Its replacement names the family by shape.&lt;/p&gt;

&lt;p&gt;The labeled lines are not a style choice, and this is the second lesson, smaller, but the one I would carry to any format you design for a parser. The field is a regex. A regex can contain any printable character, so there is no character you can put between fields that the field itself cannot also contain: no colon, no tab, no pipe, none. The old one-per-line format worked because nothing in it had happened to be ambiguous yet. The rule I wrote down afterwards: delimit a field in-band only with a character the field cannot contain by construction, imposed by the filesystem, the protocol or the language grammar, never by convention. Where no such character exists, stop delimiting; give each field its own labeled line and let the line break do the separating. And never fix a format's ambiguity with guards in the parser. Guards convert the misparses you imagined into loud ones and leave the rest exactly as silent as before.&lt;/p&gt;

&lt;p&gt;The machinery is public. &lt;a href="https://github.com/triartleet/etymd" rel="noopener noreferrer"&gt;etymd&lt;/a&gt; is the auditor; &lt;code&gt;npx etymd screen --tree&lt;/code&gt; runs the screen over a working tree, &lt;code&gt;.etymd-screen-allow&lt;/code&gt; at the repo root holds the signed exemptions, and the screen reads that file from the repo it is screening and screens the file too, because an exemption file is also content. That much you can set up today. The rest of the essay is why the four labels are not optional.&lt;/p&gt;

&lt;p&gt;What I took from the week is a test I now apply to every gate I run: for each way it could stop checking, what would tell me? If the honest answer is "a commit that should have failed", the gate's failure mode is an unaudited hole, and signing the holes you already know about is the cheapest place to start.&lt;/p&gt;

</description>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
      <category>bestpractices</category>
    </item>
  </channel>
</rss>
