<?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: untactit</title>
    <description>The latest articles on DEV Community by untactit (@untactit).</description>
    <link>https://dev.to/untactit</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%2F4073792%2Fd8ba9cbb-1019-4bb5-9480-3dd40b8c2b35.png</url>
      <title>DEV Community: untactit</title>
      <link>https://dev.to/untactit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/untactit"/>
    <language>en</language>
    <item>
      <title>Playwright's fill() threw an error and still inserted the text: the CodeMirror 6 double-paste trap</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Wed, 19 Aug 2026 08:31:34 +0000</pubDate>
      <link>https://dev.to/untactit/playwrights-fill-threw-an-error-and-still-inserted-the-text-the-codemirror-6-double-paste-trap-31ja</link>
      <guid>https://dev.to/untactit/playwrights-fill-threw-an-error-and-still-inserted-the-text-the-codemirror-6-double-paste-trap-31ja</guid>
      <description>&lt;p&gt;Browser automation has a standard reflex: if the action throws, retry it. CodeMirror 6 broke that reflex for us in the most expensive way possible — the throw and the success were the same event.&lt;/p&gt;

&lt;p&gt;This is a short field note on the failure, the mechanism as far as we could actually measure it, and the fix that made it structurally impossible to hit again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;We were automating a post editor built on CodeMirror 6, filling a long article body with Playwright's &lt;code&gt;fill()&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;fill()&lt;/code&gt; threw a timeout error&lt;/li&gt;
&lt;li&gt;Our handler treated the throw as "nothing happened" and retried&lt;/li&gt;
&lt;li&gt;The saved article came out with the entire body &lt;strong&gt;duplicated&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first &lt;code&gt;fill()&lt;/code&gt; had thrown &lt;em&gt;and&lt;/em&gt; inserted the content. The retry inserted it again. Every layer of that pipeline behaved reasonably, and the output was still wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we could verify, and what we could not
&lt;/h2&gt;

&lt;p&gt;CodeMirror 6 manages a &lt;code&gt;contenteditable&lt;/code&gt; surface through its own view model — the DOM you see and the editor state behind it are separate structures. &lt;code&gt;fill()&lt;/code&gt; inserts text, then verifies the result against its own expectation of the element's value. On a CodeMirror surface, that verification can fail even though the insertion event was fully processed.&lt;/p&gt;

&lt;p&gt;That is as far as we measured. We did not chase the internals further, because the useful lesson is not the specific bug — it is that &lt;strong&gt;"exception means not executed" is an assumption, and some editors break it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one path, then count
&lt;/h2&gt;

&lt;p&gt;We stopped trying to make the retry smarter and instead made double-insertion structurally impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Insert through one path that replaces instead of appending.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meta+A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;insertText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select-all before insert means a retry overwrites the previous attempt instead of stacking on top of it. &lt;code&gt;execCommand&lt;/code&gt; is deprecated, but it goes through the browser's editing pipeline — which is what &lt;code&gt;contenteditable&lt;/code&gt; frameworks actually listen to — and in practice it is still the most stable way in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Verify by counting, after reload.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We plant a unique marker string at the end of the body, save, reload, and count occurrences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;MARKER&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="nx"&gt;count&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`marker x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — duplicated insert`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The success check moved from "did the call return cleanly" to "does the saved artifact contain exactly one copy." Those are different questions, and only the second one is about reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For write operations, &lt;strong&gt;an exception is not proof of non-execution&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Before any retry of a write, &lt;strong&gt;re-read the current state&lt;/strong&gt; — the previous attempt may have partially landed&lt;/li&gt;
&lt;li&gt;Non-idempotent operations (append, insert, submit) deserve a &lt;strong&gt;marker you can count afterward&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Judge success from the &lt;strong&gt;saved artifact&lt;/strong&gt;, not from return values or toasts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same trap exists anywhere a "submit" or "add row" can throw after the side effect landed. When a write throws, the first question is not &lt;em&gt;why did it fail&lt;/em&gt; — it is &lt;em&gt;did it actually fail&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Field note from building &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt;, a control plane for the skills, rules, and memory AI agents run on — currently pre-launch. The product applies the same rule: a write counts as done only after the target is read back.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>24 of the 30 duplicate groups were not duplicates</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Tue, 18 Aug 2026 23:02:07 +0000</pubDate>
      <link>https://dev.to/untactit/24-of-the-30-duplicate-groups-were-not-duplicates-2m56</link>
      <guid>https://dev.to/untactit/24-of-the-30-duplicate-groups-were-not-duplicates-2m56</guid>
      <description>&lt;p&gt;We grouped an asset database by title and found 30 groups of "duplicates." Content hashing dissolved 24 of them. Five were real. One was unjudgeable — because the rows had no content at all.&lt;/p&gt;

&lt;p&gt;This is a field note on deduplication judgment, from auditing a store of AI-agent assets (conversations, skills, memories, instruction files) collected across many platforms. The numbers are from a live audit on 2026-08-19.&lt;/p&gt;

&lt;h2&gt;
  
  
  Title matching is a grouping key, not a judgment
&lt;/h2&gt;

&lt;p&gt;The 30 groups came from exact title matches. That is a fine way to &lt;em&gt;find candidates&lt;/em&gt;. It is a terrible way to &lt;em&gt;decide&lt;/em&gt;. For each group we judged on four axes instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;sha256 of the full content&lt;/strong&gt; — identical bytes end the discussion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Similarity of the stripped body&lt;/strong&gt; — headers removed, then compared&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance&lt;/strong&gt; — which system claims to have produced the row&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creation-time gap&lt;/strong&gt; — seconds apart and months apart mean different things&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how the 30 groups actually broke down.&lt;/p&gt;

&lt;h2&gt;
  
  
  5 groups: real duplicates
&lt;/h2&gt;

&lt;p&gt;Identical content, twice in the store. Every one had a mundane cause: the same asset written under a underscore slug and a hyphen slug; the same memory saved once under an English key and once under a localized key; the same body captured under two different provenance IDs.&lt;/p&gt;

&lt;p&gt;Real duplicates are boring. That is the point — they are the minority, and they are the only rows you may safely collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  6 groups: same title, different asset
&lt;/h2&gt;

&lt;p&gt;Two conversations titled identically, created 45 days apart, 8% body similarity. A 213KB thread and a 27KB thread sharing a name. One pair sat at 85% similarity with a &lt;strong&gt;ten-month&lt;/strong&gt; gap — almost certainly a person pasting an old conversation to continue it, which makes it two assets, not one.&lt;/p&gt;

&lt;p&gt;Collapse these and you destroy history. Title reuse is normal human behavior, not corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  7 groups: probes that look like twins
&lt;/h2&gt;

&lt;p&gt;Verification probes — synthetic assets written to test a pipeline — differ only by an embedded marker. Two probes from different runs look like duplicates to every fuzzy matcher. They are separate &lt;em&gt;events&lt;/em&gt;, and folding them together erases the record of one run.&lt;/p&gt;

&lt;p&gt;If your system writes test artifacts into real storage, your dedup logic needs to know what a probe looks like. Ours does now.&lt;/p&gt;

&lt;h2&gt;
  
  
  12 rows in 4 groups: nothing to judge
&lt;/h2&gt;

&lt;p&gt;The strangest finding: rows whose body was &lt;strong&gt;entirely empty&lt;/strong&gt; — header only. Same titles as real assets, no content. All of them were created inside one window of about 8.7 minutes on a single afternoon, up to five copies of one title, seconds apart.&lt;/p&gt;

&lt;p&gt;That pattern is not a duplicate problem. It is the fossil record of a write-path incident — something retried row creation without writing bodies. The dedup lesson: &lt;strong&gt;when the only evidence you have is absence, you cannot classify.&lt;/strong&gt; These rows go to quarantine, not to a merge queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The provenance field was lying too
&lt;/h2&gt;

&lt;p&gt;Three pairs shared the same provenance reference while holding completely unrelated content — different platforms, different topics, different everything. The field that was supposed to anchor identity had collisions.&lt;/p&gt;

&lt;p&gt;Any pipeline that had trusted provenance alone to say "already collected, skip" would have silently dropped real assets. We found the collisions only because content hashing disagreed with the reference field — two signals, arguing, is what an audit is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Groups&lt;/th&gt;
&lt;th&gt;Safe action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real duplicate (byte-identical)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Archive one copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same title, different asset&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Keep both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct test probes&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Keep both, tag as probes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty-shell rows&lt;/td&gt;
&lt;td&gt;4 (12 rows)&lt;/td&gt;
&lt;td&gt;Quarantine, investigate the writer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provenance collisions&lt;/td&gt;
&lt;td&gt;3 pairs&lt;/td&gt;
&lt;td&gt;Re-verify at the source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;80% of what looked like duplication was not. The uncomfortable inverse also holds: a store that "has no duplicates" by title may be full of byte-identical content under different names. Both errors come from the same root — judging identity by the cheapest available key.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Notes from building &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt; — pre-launch — where asset identity across platforms is the whole job. Content hash plus provenance plus read-back verification; never title alone.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>programming</category>
      <category>ai</category>
      <category>datascience</category>
    </item>
    <item>
      <title>The ledger said 763 assets. The platforms said about 400.</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Tue, 18 Aug 2026 23:02:02 +0000</pubDate>
      <link>https://dev.to/untactit/the-ledger-said-763-assets-the-platforms-said-about-400-a03</link>
      <guid>https://dev.to/untactit/the-ledger-said-763-assets-the-platforms-said-about-400-a03</guid>
      <description>&lt;p&gt;A sync ledger fails silently. Ours said 763 assets. When we stopped trusting it and counted by hand — opening 21 AI platforms in browser tabs, calling each platform's own API from the page context, comparing ID by ID — roughly 400 of those were real.&lt;/p&gt;

&lt;p&gt;This is the audit log of that gap. The numbers are from one working account, measured on 2026-08-18. Nothing here is hypothetical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The database held every asset collected from the platforms a working account actually uses — conversations, projects, skills, instruction files, memories — across Claude, ChatGPT, Gemini, Grok, Perplexity, Kimi, Qwen and fourteen others. 763 rows total.&lt;/p&gt;

&lt;p&gt;The verification method matters more than the tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call the platform's own list API from inside an authenticated page (&lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;credentials: 'include'&lt;/code&gt;), paginating to exhaustion.&lt;/li&gt;
&lt;li&gt;Cross-check against what the UI actually shows.&lt;/li&gt;
&lt;li&gt;Where possible, compare &lt;strong&gt;ID sets&lt;/strong&gt; — intersection and both differences — not counts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A count that matches can still be two different lists. Only the ID diff tells you what is actually there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways a ledger lies
&lt;/h2&gt;

&lt;p&gt;The 363-row gap was not one bug. It was three independent failure modes, each of which produces a database that looks healthy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Confirmed rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ghosts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deleted on the platform, still alive in the DB&lt;/td&gt;
&lt;td&gt;at least 173&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Misses&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exists on the platform, never made it into the DB&lt;/td&gt;
&lt;td&gt;at least 63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;False positives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Platform-provided defaults counted as user assets&lt;/td&gt;
&lt;td&gt;at least 109&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the shape of the problem: ghosts and false positives inflate the ledger, misses deflate it. They partially cancel. &lt;strong&gt;A total count is the one metric guaranteed to hide all three.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three worst cells
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A page-size limit became a count.&lt;/strong&gt; One platform's skill list showed 100 in the DB. The user had installed 4. The other 96 were the platform's public catalog — shown to every user — and the number 100 was simply the API's page-size cap, recorded as if it were a fact about the account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example strings became memories.&lt;/strong&gt; A memory collector stored 2 items for a platform where the user had 8 real saved memories. It had missed all 8 — and stored two lines of the settings page's placeholder text ("e.g. ...") as if they were user data. On the dashboard, this cell looked like one of the working ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Files that were never there.&lt;/strong&gt; 42 instruction rows pointed at local rule directories that did not exist on the machine at all. Nothing had been deleted; the source path had simply never existed on this host. The collector wrote 42 rows anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "the sync ran successfully" means nothing
&lt;/h2&gt;

&lt;p&gt;Every one of these rows was written by a run that reported success. That is the actual lesson:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Success of the write says nothing about truth of the read.&lt;/strong&gt; The collector faithfully stored what it fetched. What it fetched was a catalog page, a placeholder string, a stale cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination that stops early looks identical to pagination that finished.&lt;/strong&gt; 63 real assets were missing because list calls stopped at page one and the UI showed a green check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts drift toward plausible.&lt;/strong&gt; 763 is a believable number for a heavy user. Nobody questions a believable number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What actually fixes it
&lt;/h2&gt;

&lt;p&gt;The fix is not a better collector. It is a &lt;strong&gt;read-back audit&lt;/strong&gt; as a first-class operation: after any sync, re-read the platform and diff IDs in both directions, and treat "platform-provided default" as a category your matcher must know about. Run it on a schedule, not once.&lt;/p&gt;

&lt;p&gt;If you run agents across several tools, try the count yourself. Pick one platform, list what your ledger thinks is there, then page through the platform's own API to the end. The first diff usually takes ten minutes and is rarely empty.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This audit is part of building &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt;, a control plane for the skills, rules, and memory AI agents run on — currently pre-launch. The read-back verification described here is the product lesson: deployment without re-reading the target is how ledgers rot.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>testing</category>
      <category>database</category>
    </item>
    <item>
      <title>AGENTS.md was supposed to end the sprawl. I counted the files on one laptop.</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:57:32 +0000</pubDate>
      <link>https://dev.to/untactit/agentsmd-was-supposed-to-end-the-sprawl-i-counted-the-files-on-one-laptop-5e4l</link>
      <guid>https://dev.to/untactit/agentsmd-was-supposed-to-end-the-sprawl-i-counted-the-files-on-one-laptop-5e4l</guid>
      <description>&lt;p&gt;AGENTS.md solved the format argument. Codex CLI, GitHub Copilot, Cursor, Windsurf, Amp and Devin&lt;br&gt;
all read the same file now. Claude Code still reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, Gemini CLI still reads &lt;code&gt;GEMINI.md&lt;/code&gt;,&lt;br&gt;
and that exception list keeps getting shorter.&lt;/p&gt;

&lt;p&gt;The format was never the expensive part.&lt;/p&gt;

&lt;p&gt;I scanned one working laptop this morning. Nothing exotic — one developer, a few years of projects,&lt;br&gt;
the normal agent tooling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;file&lt;/th&gt;
&lt;th&gt;copies on disk&lt;/th&gt;
&lt;th&gt;distinct contents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GEMINI.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SKILL.md&lt;/code&gt; (agent skills)&lt;/td&gt;
&lt;td&gt;3,073&lt;/td&gt;
&lt;td&gt;887&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Method: &lt;code&gt;find ~ -maxdepth 7 -type f -name &amp;lt;X&amp;gt;&lt;/code&gt;, pruning &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.venv*&lt;/code&gt;, &lt;code&gt;site-packages&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;Library/Caches&lt;/code&gt;. "Distinct contents" is the count of unique md5 hashes. Vendor-installed skill&lt;br&gt;
bundles are included in the SKILL.md row, which is exactly the point — they are on the machine and&lt;br&gt;
an agent can read them.&lt;/p&gt;

&lt;p&gt;111 files. 19 different things being said.&lt;/p&gt;

&lt;p&gt;Nested files are a feature. A monorepo &lt;em&gt;should&lt;/em&gt; scope instructions per package, and the spec says so.&lt;br&gt;
But 19 distinct contents across 111 copies is not scoping. That is copy, edit one copy, forget.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the copies come from
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The hand copy.&lt;/strong&gt; You write a rule you like. You paste it into the next repo. Six months later you&lt;br&gt;
fix a typo in one of them. Every other copy is now wrong and nothing tells you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The machine boundary.&lt;/strong&gt; Global config lives outside every repo: &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;~/.gemini/GEMINI.md&lt;/code&gt;, &lt;code&gt;~/.config/opencode/AGENTS.md&lt;/code&gt;. Two laptops, two versions, and the difference&lt;br&gt;
surfaces as "it behaves differently on my machine" three weeks later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The generator.&lt;/strong&gt; The common fix is to keep one source file and generate the rest —&lt;br&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; → &lt;code&gt;CLAUDE.md&lt;/code&gt; → &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt; → &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;. It works on&lt;br&gt;
the day you run it. It does not survive the next person who edits a generated file directly, because&lt;br&gt;
nothing on disk marks a file as generated, and nothing checks.&lt;/p&gt;

&lt;p&gt;I wrote about that failure mode separately:&lt;br&gt;
&lt;a href="https://untactit.com/generated-rules-still-drift" rel="noopener noreferrer"&gt;Generating your agent rules from one file does not stop them drifting&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where each agent actually looks
&lt;/h2&gt;

&lt;p&gt;Worth having in one table, because the paths are the thing people get wrong:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tool&lt;/th&gt;
&lt;th&gt;file it reads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; (project root, subdirectories, &lt;code&gt;~/.claude/&lt;/code&gt;), &lt;code&gt;.claude/skills/*/SKILL.md&lt;/code&gt;, &lt;code&gt;.claude/agents/*.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; (root + nested, nearest wins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;, scoped &lt;code&gt;.github/instructions/*.instructions.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt; (legacy &lt;code&gt;.cursorrules&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windsurf&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.windsurf/rules/*.md&lt;/code&gt; (legacy &lt;code&gt;.windsurfrules&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GEMINI.md&lt;/code&gt; (&lt;code&gt;~/.gemini/&lt;/code&gt;, workspace root, subdirectories)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opencode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt;, falls back to &lt;code&gt;CLAUDE.md&lt;/code&gt;, global at &lt;code&gt;~/.config/opencode/AGENTS.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  A script that shows you the spread
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;agent-drift&lt;/code&gt; is a single-file Python script, MIT, no dependencies. It walks a tree, finds every&lt;br&gt;
instruction file the agents above read, groups them by content hash and prints the ones that disagree.&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;-O&lt;/span&gt; https://raw.githubusercontent.com/untactit/agent-drift/main/agent_drift.py
python3 agent_drift.py ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/untactit/agent-drift" rel="noopener noreferrer"&gt;https://github.com/untactit/agent-drift&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It does not fix anything. It tells you how bad it is, which is the part most teams are missing —&lt;br&gt;
you cannot argue for a process change without a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;I work on &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt;. One place to hold the skills, rules and memory your agents&lt;br&gt;
run on, review a change once, and have it land in every target without anyone opening a file.&lt;/p&gt;

&lt;p&gt;The distinction that matters: &lt;strong&gt;a person decides what gets approved. Nothing after that is touched by&lt;br&gt;
hand.&lt;/strong&gt; Drift is not something we repair afterwards — it stops being produced, because nobody is&lt;br&gt;
editing copies any more.&lt;/p&gt;

&lt;p&gt;It is pre-launch. The scanner above is useful without it, and that is deliberate.&lt;br&gt;
If you want the longer version of the argument, it is on the&lt;br&gt;
&lt;a href="https://untactit.com/product" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you run the scan on your own machine, I would like to see the numbers. My guess is that the&lt;br&gt;
AGENTS.md row is worse than mine on any team of more than three people.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why your AI agent instruction files drift across machines</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:35:16 +0000</pubDate>
      <link>https://dev.to/untactit/why-your-ai-agent-instruction-files-drift-across-machines-23kb</link>
      <guid>https://dev.to/untactit/why-your-ai-agent-instruction-files-drift-across-machines-23kb</guid>
      <description>&lt;p&gt;You write &lt;code&gt;CLAUDE.md&lt;/code&gt; once. Six months later there are eighteen of them on your laptop and you cannot say which one is right.&lt;/p&gt;

&lt;p&gt;This is not a hypothetical. I scanned my own machine and found exactly that. What follows is why it happens, and why the obvious fix does not fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The copy is created by the tool, not by you
&lt;/h2&gt;

&lt;p&gt;Every agent reads instructions from a path it owns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex / others&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You did not decide to keep four copies. You decided to use four tools. The copies came with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drift is not caused by carelessness
&lt;/h2&gt;

&lt;p&gt;Three mechanics produce it, none of which involve anyone being sloppy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editing at the point of use.&lt;/strong&gt; An agent misbehaves mid-task. You fix the instruction file that is open in front of you. That file is one of four, and the fix lands in one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The checkout is not the unit.&lt;/strong&gt; You have the same repo cloned twice — one for the main branch, one for a long-lived branch. Both have instruction files. They diverge the moment either is touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Home directory rules are invisible.&lt;/strong&gt; &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; belongs to no repository. It is in no CI. It is where the rules you did not want to argue about in review end up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating from one source does not close it
&lt;/h2&gt;

&lt;p&gt;The natural response is to stop keeping copies: write &lt;code&gt;AGENTS.md&lt;/code&gt; once and generate the rest.&lt;/p&gt;

&lt;p&gt;I built that. It works. It is &lt;a href="https://github.com/untactit/agent-fanout" rel="noopener noreferrer"&gt;agent-fanout&lt;/a&gt; — single-file Python, zero dependencies, MIT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 agent_fanout.py

create    CLAUDE.md
create    .cursor/rules/from-agents-md.mdc
create    .github/copilot-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated files carry a header so nobody edits them directly, and &lt;code&gt;--check&lt;/code&gt; in CI turns a hand-edited derivative into a red build.&lt;/p&gt;

&lt;p&gt;That covers &lt;strong&gt;this repository, where CI runs.&lt;/strong&gt; Which sounds like everything until you list what falls outside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global config in the home directory — no repo, no CI&lt;/li&gt;
&lt;li&gt;The window between edits — an agent reads a file the moment it changes; CI notices on push, if there is a push&lt;/li&gt;
&lt;li&gt;Repos with no CI — prototypes, throwaway clones, last week's experiment. The place instructions get rewritten most freely and gated least&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unit is the machine, not the repo.&lt;/strong&gt; The agent runs on a laptop holding many checkouts, several copies of the same repo, and a home directory. Anything scoped to one repository cannot see across that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So measure, do not only prevent
&lt;/h2&gt;

&lt;p&gt;Prevention is policy. Detection is measurement. Policy gets bypassed in ways you cannot see unless you measure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/untactit/agent-drift" rel="noopener noreferrer"&gt;agent-drift&lt;/a&gt; scans paths rather than repos, and groups by content similarity rather than filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 agent_drift.py ~/work ~/side-projects

Scanned 47 instruction files.
DRIFT: 2 documents, 5 distinct versions between them.

  claude-code:CLAUDE.md
    6 copies, 3 versions
      9b01aeaa204d  3 files, 406 lines
      2dc3c616c279  2 files, 411 lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filename matching is useless here. Unrelated projects having different &lt;code&gt;CLAUDE.md&lt;/code&gt; files is correct, and a tool that reports that as drift stops being used. Point it at your whole working directory, not one project — the interesting results cross repository boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two jobs, not one
&lt;/h2&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;Scope&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;agent-fanout&lt;/td&gt;
&lt;td&gt;one repository&lt;/td&gt;
&lt;td&gt;are the derived files current&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-drift&lt;/td&gt;
&lt;td&gt;whole machine, many paths&lt;/td&gt;
&lt;td&gt;where did copies diverge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation removes the reason copies exist. Detection catches the copies that exist for reasons you did not anticipate. Doing only one leaves you &lt;strong&gt;feeling covered&lt;/strong&gt;, which is worse than knowing you are not. I walked into that myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this ends
&lt;/h2&gt;

&lt;p&gt;Honestly, the end state is that neither script is needed — assets do not sit scattered as files on a machine, and one reviewed copy reaches every machine. That is what I am building at &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt;, currently pre-launch.&lt;/p&gt;

&lt;p&gt;The scripts do not depend on it. Take whichever part is useful.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Generating your agent rules from one file does not stop them drifting</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:51:21 +0000</pubDate>
      <link>https://dev.to/untactit/generating-your-agent-rules-from-one-file-does-not-stop-them-drifting-3gpb</link>
      <guid>https://dev.to/untactit/generating-your-agent-rules-from-one-file-does-not-stop-them-drifting-3gpb</guid>
      <description>&lt;p&gt;The obvious fix for "our agent instruction files keep diverging" is to stop maintaining copies. Write &lt;code&gt;AGENTS.md&lt;/code&gt;, generate &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt; and &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; from it, done.&lt;/p&gt;

&lt;p&gt;I built that. It works. It also does not solve the problem, and the gap between those two statements is worth spelling out, because I only saw it after running the thing on a real machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generation actually covers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 agent_fanout.py

create    CLAUDE.md
create    .cursor/rules/from-agents-md.mdc
create    .github/copilot-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One source, several derived files, a header on each so nobody edits the derived copy by accident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Generated from AGENTS.md by agent-fanout. Do not edit this file. --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to CI and the pull request that edits &lt;code&gt;CLAUDE.md&lt;/code&gt; directly turns the build red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 agent_fanout.py . --check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers &lt;strong&gt;this repository, on the machines that run CI&lt;/strong&gt;. Which sounds like everything until you list what it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not cover
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Other repositories.&lt;/strong&gt; Your team has more than one. Each has its own &lt;code&gt;AGENTS.md&lt;/code&gt;, and they were copy-pasted from each other at some point. Generation keeps each repo internally consistent while the repos drift apart from one another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global config.&lt;/strong&gt; Claude Code reads &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; in addition to the project file. Cursor has user-level rules. Those live outside any repository, are never in CI, and are exactly where people put the rule they didn't want to argue about in review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The window between edits.&lt;/strong&gt; Generation runs when someone runs it. Between that moment and the next CI run, a derived file can be edited and used. The agent reads it immediately; CI notices on push, if there is a push.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repos without CI.&lt;/strong&gt; Prototypes, scratch clones, the repo someone made last Tuesday. Those are where instructions get freely modified, and they are the ones with no gate at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machines, not repositories.&lt;/strong&gt; The unit that runs an agent is a laptop. A laptop has many checkouts, several of the same repo, and a home directory. Nothing that operates per-repository can see across that.&lt;/p&gt;

&lt;h2&gt;
  
  
  So you need to look, not just prevent
&lt;/h2&gt;

&lt;p&gt;Prevention is a policy. Detection is a measurement. Policies get bypassed in ways that are invisible until you measure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 agent_drift.py ~/work ~/side-projects

Scanned 47 instruction files.

DRIFT: 2 documents, 5 distinct versions between them.

  claude-code:CLAUDE.md
    6 copies, 3 versions
      9b01aeaa204d  3 files, 406 lines
      2dc3c616c279  2 files, 411 lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one scans paths rather than repositories, and groups files by content similarity rather than by filename — two unrelated projects having different &lt;code&gt;CLAUDE.md&lt;/code&gt; files is not drift, and reporting it as drift makes the output worthless. (Getting that grouping right took three rewrites; &lt;a href="https://dev.to/untactit/i-found-18-versions-of-the-same-claudemd-on-one-laptop-bbc"&gt;I wrote that part up separately&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Run it across your whole working directory, not one project. The interesting results are the ones that cross repository boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pairing
&lt;/h2&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;Scope&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-fanout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one repository&lt;/td&gt;
&lt;td&gt;"are the derived files current?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-drift&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;whole machine, many paths&lt;/td&gt;
&lt;td&gt;"where did copies diverge anyway?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation removes the &lt;em&gt;reason&lt;/em&gt; copies exist. Detection catches the copies that exist for reasons you did not anticipate. Neither is redundant, and doing only the first one gives you a false sense of coverage — which is the actual failure mode I want to warn about, because it is the one I walked into.&lt;/p&gt;

&lt;p&gt;Both are single-file Python, no dependencies, read-only where it matters, MIT:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/untactit/agent-fanout" rel="noopener noreferrer"&gt;agent-fanout&lt;/a&gt; — write &lt;code&gt;AGENTS.md&lt;/code&gt; once, generate the rest&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/untactit/agent-drift" rel="noopener noreferrer"&gt;agent-drift&lt;/a&gt; — find the copies that no longer agree&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The honest end state is that neither script is necessary, because the assets are not files sitting on laptops at all — they live in one reviewed place and reach every machine without anyone copying anything. That is what I am building at &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt;, currently pre-launch.&lt;/p&gt;

&lt;p&gt;The scripts stand on their own and don't depend on it. Use them, ignore the rest.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I found 18 versions of the same CLAUDE.md on one laptop</title>
      <dc:creator>untactit</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:29:37 +0000</pubDate>
      <link>https://dev.to/untactit/i-found-18-versions-of-the-same-claudemd-on-one-laptop-bbc</link>
      <guid>https://dev.to/untactit/i-found-18-versions-of-the-same-claudemd-on-one-laptop-bbc</guid>
      <description>&lt;p&gt;Your team runs Claude Code, Cursor, and Copilot. Each of them reads a file before it acts — &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt;, &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;. Those files get copied: into another repo, onto another laptop, into someone's scratch directory.&lt;/p&gt;

&lt;p&gt;Then somebody edits a copy.&lt;/p&gt;

&lt;p&gt;Nothing errors. No warning, no diff, no failing test. The agent on that machine just behaves differently — a rule someone removed six weeks ago, a convention only half the team's agents know about. It shows up as "works on my machine" with nothing to bisect.&lt;/p&gt;

&lt;p&gt;I wrote a script to find those files and tell you which copies disagree.&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;-O&lt;/span&gt; https://raw.githubusercontent.com/untactit/agent-drift/main/agent_drift.py
python3 agent_drift.py ~/work
&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;Scanned 47 instruction files.

DRIFT: 2 documents, 5 distinct versions between them.

  claude-code:CLAUDE.md
    6 copies, 3 versions
      9b01aeaa204d  3 files, 406 lines
        ~/work/api/CLAUDE.md
        ~/work/web/CLAUDE.md
      2dc3c616c279  2 files, 411 lines
        ~/work/infra/CLAUDE.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file, standard library only, Python 3.8+. Reads only — it never writes to the files it scans. MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part is what I got wrong
&lt;/h2&gt;

&lt;p&gt;The naive version took me ten minutes and was useless. The three fixes are the whole story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grouping by filename is wrong
&lt;/h3&gt;

&lt;p&gt;First version: collect every &lt;code&gt;CLAUDE.md&lt;/code&gt;, hash them, report the ones that differ.&lt;/p&gt;

&lt;p&gt;Ran it on my own machine. It reported &lt;strong&gt;21 files as one document with 18 versions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Of course it did. Unrelated projects have unrelated instructions. They were never supposed to match. A tool that reports that as drift is noise, and a noisy tool is worse than no tool — you stop reading it by the third run.&lt;/p&gt;

&lt;p&gt;Files are the same document when their &lt;strong&gt;content&lt;/strong&gt; says so, not when their filename does.&lt;/p&gt;

&lt;h3&gt;
  
  
  A bag of words is not enough either
&lt;/h3&gt;

&lt;p&gt;So: normalise whitespace, tokenise, compare with Jaccard similarity, threshold 0.7.&lt;/p&gt;

&lt;p&gt;Still one giant cluster. I measured actual pairs to find out why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;012Hki23QBMr ↔ 016ct6suZ14M : 0.000
012Hki23QBMr ↔ 016avoYh97Eu : 0.764
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instruction files written for the same tool share almost all of their vocabulary. &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;skill&lt;/code&gt;, &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;never&lt;/code&gt;, &lt;code&gt;always&lt;/code&gt;. Two documents with nothing in common still overlap heavily as word sets.&lt;/p&gt;

&lt;p&gt;Switched to &lt;strong&gt;5-word shingles&lt;/strong&gt; — the set of every consecutive 5-word phrase. Vocabulary can collide; phrasing rarely does. Re-measured: unrelated pairs dropped to 0.000, genuinely forked documents landed at 0.53–0.76. That separation is what you want, and it came from measuring, not from reasoning about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-linkage clustering chains everything together
&lt;/h3&gt;

&lt;p&gt;Still one cluster. This one is a classic.&lt;/p&gt;

&lt;p&gt;Union-find with "similar to any member" means A joins B, B joins C, and now A and C are in the same cluster despite sharing nothing. One weak link and the cluster swallows the tree.&lt;/p&gt;

&lt;p&gt;Each cluster now keeps a &lt;strong&gt;representative&lt;/strong&gt; — its largest member — and a file joins only if it resembles that representative directly. Clusters stay coherent regardless of scan order.&lt;/p&gt;

&lt;p&gt;Final result on my machine: 15 files, 9 of them 0.53–0.65 similar to the representative. Those really are forks of one template. Correct detection, and the number is small enough to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it in CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 agent_drift.py . --fail-on-drift&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 1 when drift exists. The build goes red the day two copies of your team's instructions stop matching, instead of you finding out in six months when someone asks why the agent stopped following a rule.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--json&lt;/code&gt; gives you the full report if you'd rather post-process it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it covers
&lt;/h2&gt;

&lt;p&gt;Claude Code, Codex/&lt;code&gt;AGENTS.md&lt;/code&gt;, Cursor, GitHub Copilot, Gemini, Windsurf, Cline, Aider, Continue, Zed. The patterns are one constant at the top of the file — PR or issue if yours is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  After you find it
&lt;/h2&gt;

&lt;p&gt;Detection is the easy half. The copies exist for a reason, and until that reason is gone the drift comes back. What removes it is one reviewed source that reaches every machine without anyone pasting anything.&lt;/p&gt;

&lt;p&gt;That's the problem I'm building &lt;a href="https://untactit.com" rel="noopener noreferrer"&gt;untactit&lt;/a&gt; around — it's pre-launch. This script doesn't depend on it and never will.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/untactit/agent-drift" rel="noopener noreferrer"&gt;github.com/untactit/agent-drift&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
