<?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: Sam LABBE</title>
    <description>The latest articles on DEV Community by Sam LABBE (@slabb).</description>
    <link>https://dev.to/slabb</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%2F2901084%2F16244fc1-095c-41d9-ad2a-203fb3e617f9.png</url>
      <title>DEV Community: Sam LABBE</title>
      <link>https://dev.to/slabb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/slabb"/>
    <language>en</language>
    <item>
      <title>Every "no AI contributions" policy is a Level 0 control</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Mon, 12 Oct 2026 18:02:21 +0000</pubDate>
      <link>https://dev.to/slabb/every-no-ai-contributions-policy-is-a-level-0-control-1a99</link>
      <guid>https://dev.to/slabb/every-no-ai-contributions-policy-is-a-level-0-control-1a99</guid>
      <description>&lt;p&gt;This week QGIS merged &lt;a href="https://github.com/qgis/QGIS-Enhancement-Proposals/pull/387" rel="noopener noreferrer"&gt;QEP #387&lt;/a&gt;: "No LLM, AI or agent assisted code contributions are allowed in the qgis/qgis repository." Violators face "code reversion and contributor rights sanctioning." It replaced a human-in-the-loop policy, it passed a vote, it is on master as of October 7. The HN thread is at three hundred comments and counting, split between "rationing attention is fair" and "this will age like milk."&lt;/p&gt;

&lt;p&gt;The line that settles the whole debate was written by the project itself, in the proposal's discussion. Asked how compliance will work, a core contributor answered: &lt;em&gt;did you in good conscience write the code yourself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is not an enforcement mechanism. That is a conscience test with a reversion clause attached — and by the taxonomy this series has been building, it makes QEP #387 a textbook &lt;strong&gt;Level 0 control&lt;/strong&gt;: a paragraph of English that nobody executes, failing silently, with the drift invisible. The project knows it; the enforcement quote is honest about it. What the debate is missing is the ladder — and what the second rung costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grant the ban first, because it is rational
&lt;/h2&gt;

&lt;p&gt;Before grading anything: the workload is real and the maintainers are not imagining it. The proposal's rationale — "massive and unbearable pressure", AI volume exceeding "what human review can take seriously" — matches what every maintainer in every lane reports. And this is not QGIS's first control attempt: an earlier tagging process for AI-assisted PRs was tried and quietly died, with roughly fifty PRs tagged and forty closed. They tried the lighter control. It did not hold. Nyall Dawson's "start with a closed door" and open selectively is a stop gap, said out loud by a voter: "the baby might be non-viable."&lt;/p&gt;

&lt;p&gt;A blanket ban is the correct answer to the question "how do we survive this quarter." This post is about the next question: what is the policy &lt;em&gt;made of&lt;/em&gt;, and what does that material cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ladder, applied
&lt;/h2&gt;

&lt;p&gt;Every control lives on one of three rungs, graded by what enforces it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 0 — prose.&lt;/strong&gt; "No AI contributions permitted" in a document nobody executes. Fails silently: the drift is invisible, because the trigger condition — &lt;em&gt;was this code AI-assisted?&lt;/em&gt; — cannot be evaluated by anything except a conscience. The definitional holes in the QGIS thread are not drafting accidents; they are proof the trigger is unevaluable. Does autocomplete count? Translation? A ranking model that reordered your imports? A rule whose scope is undecidable cannot be enforced by anything below a human's honest memory — and the project has already said that is the mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1 — a primitive in context.&lt;/strong&gt; A declaration the contributor makes, visible in the PR: a template field, a trailer line, a checkbox that survives into the commit. It fails visibly — but only if someone looks. QGIS tried this rung: the tagging process. It failed for the boring reason — roughly fifty tags in the whole history is not enforcement, it is decoration. Level 1 works when the CI checks it, and QGIS's did not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2 — the host refuses.&lt;/strong&gt; The control lives in a gate that runs without trusting anyone's memory. This is the rung the debate keeps skipping, because people conflate two different problems: &lt;strong&gt;detecting AI code&lt;/strong&gt; (adversarial, unwinnable — simple code is undetectable and always will be) and &lt;strong&gt;binding process to a signed name&lt;/strong&gt; (boring, winnable, and what receipts are for).&lt;/p&gt;

&lt;p&gt;The receipt schema this series follows ships the distinction in code. Every verdict must carry &lt;code&gt;judged_by&lt;/code&gt; — required, no default; the schema throws rather than seal an unlabeled verdict. For LLM-judged checks, &lt;code&gt;spec_hash&lt;/code&gt; is mandatory: the prompt that defines the judgment is hashed, so the rubric and its paraphrase produce different ids. Neither field claims to detect a lie. They make an unlabeled claim &lt;em&gt;unsealable&lt;/em&gt; — the lie, if it comes, has to arrive signed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measured part: twenty repos, six policies, zero enforcement
&lt;/h2&gt;

&lt;p&gt;A taxonomy without data is a sermon, so I audited the twenty most-starred repositories on GitHub — the projects whose policies shape what thousands of contributors believe is normal. The method, disclosed: pull each repo's contributing docs and AI policy files, read every policy found in full, grade it on the ladder, and check the repos' GitHub workflow names for any automation referencing AI, LLMs, or disclosure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fourteen of the twenty have no AI policy anywhere in their contributing docs&lt;/strong&gt; — including several that receive AI-generated PRs by the hundred. Six have one. I read all six in full, and every one of them, graded on the ladder, stops below the gate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Policy, as written&lt;/th&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Machine enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;denoland/deno&lt;/td&gt;
&lt;td&gt;AI-assisted contributions: "you &lt;strong&gt;must disclose&lt;/strong&gt; this in your PR description"; PRs "will be rejected if there is suspicion of undisclosed AI usage"&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pytorch/pytorch&lt;/td&gt;
&lt;td&gt;AI content "must be clearly disclosed and contained" under a labeled PR section, with human commentary attached&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nodejs/node&lt;/td&gt;
&lt;td&gt;contributors "should disclose" the tools used "and what the contributor does to personally verify the generated output"; DCO still applies&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;home-assistant/core&lt;/td&gt;
&lt;td&gt;unreviewed AI content "will not be accepted"; autonomous agents banned, PRs closed on belief&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flutter/flutter&lt;/td&gt;
&lt;td&gt;a behaviors policy — "focuses on behaviors rather than tools", conduct channel for violations&lt;/td&gt;
&lt;td&gt;0–1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rust-lang/rust&lt;/td&gt;
&lt;td&gt;conditional allow — and self-labeled: "we are aware that many clauses in this policy are unenforceable"&lt;/td&gt;
&lt;td&gt;0–1, labeled&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qgis/QGIS&lt;/td&gt;
&lt;td&gt;the ban: reversion + sanctioning, trigger = conscience&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The other thirteen have nothing to grade. The "machine enforcement" column is the absence finding: I checked each repo's workflow names for anything referencing AI, disclosure, or contribution labels — nothing. A deeper CI-content audit might hide an exception; the name-level sweep found none.)&lt;/p&gt;

&lt;p&gt;Three findings, in ascending order of interest. &lt;strong&gt;Deno, node, and PyTorch converge on disclosure&lt;/strong&gt; — the ecosystem's consensus rung is "say it out loud", and PyTorch goes furthest: AI content must be &lt;em&gt;contained&lt;/em&gt; under a labeled PR-template section with human commentary attached. That is the closest thing to structure anyone ships — the template makes the section exist by default; an empty one is at least visible. &lt;strong&gt;Home Assistant closes PRs on belief&lt;/strong&gt; — "we believe were created autonomously" — which is the strongest trigger in the sample, applied by the weakest instrument. And &lt;strong&gt;Rust wrote the thesis of this post before I did&lt;/strong&gt;: "we are aware that many clauses in this policy are unenforceable. Our goal is not to catch every violation... our goal is to remove plausible deniability: to force a choice between following the policy and intentionally violating it."&lt;/p&gt;

&lt;p&gt;Rust is the proof the ladder is fair. Its policy is prose — Level 0 by trigger — and it says so, out loud, in its own introduction. QGIS threatens reversion and sanctioning from the same rung without the label. Same material, opposite honesty: one wrote "these clauses are unenforceable" where its enforcement should be; the other dressed the conscience test in reversion threats. The difference between them is not strictness. It is whether the policy's own rung is falsifiable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43u1n8gfyo1nzd9ojiyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43u1n8gfyo1nzd9ojiyg.png" alt="three rungs applied to QGIS : prose (QEP #387), dead tagging (~50/~40), CI gate" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Level 2 policy for AI contributions looks like
&lt;/h2&gt;

&lt;p&gt;Not detection. Process, signed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A provenance trailer on every commit&lt;/strong&gt;, the same shape as &lt;code&gt;Signed-off-by&lt;/code&gt;: &lt;code&gt;Authored-by: human&lt;/code&gt; or &lt;code&gt;Authored-by: human, assisted-by: &amp;lt;tool&amp;gt;&lt;/code&gt;. No LLM-detection claims — a &lt;em&gt;declaration&lt;/em&gt;, required by the CI, refused at the gate when absent. The Linux kernel answer quoted approvingly on HN — "LLM use allowed, but you are fully responsible for the code it produces" — is this rung: responsibility attaching to a signed name instead of to a detection heuristic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversion with a logged reason.&lt;/strong&gt; "Code reversion and sanctioning" already threatened by QGIS becomes enforceable the day reversions carry receipts: which commit, which rule, which reviewer. A sanction that lives in a private conversation is not a control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A published exceptions log.&lt;/strong&gt; Dawson's "closed door, open case-by-case" (autocomplete replacements, translations) is the right shape — but a case-by-case door with no public log is a door that silently becomes a hallway. The exceptions ledger &lt;em&gt;is&lt;/em&gt; the policy's audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional, and only if the maintainers want it: canaries for docs.&lt;/strong&gt; Plant a decoy instruction in the contributing docs and see who commits it — the deliberately-failed probe, already proven useful for grading agents. Cheap, imperfect, honest about being a tripwire rather than a judge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the rung into the policy.&lt;/strong&gt; Rust's LLM policy does it in its own overview — "we are aware that many clauses in this policy are unenforceable" — and that sentence is why nobody can accuse Rust of pretending. A labeled weakness beats an invisible one, in constitutions as much as in code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this detects AI code. Detection is the adversary's game and the adversary is winning it. The receipts regime claims something narrower and true: &lt;strong&gt;when a contribution breaks the rules, the break is provable, attributable, and on the record&lt;/strong&gt; — instead of discoverable, if at all, by vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost, stated plainly
&lt;/h2&gt;

&lt;p&gt;The seatbelt tax. A provenance trailer is thirty seconds per commit, forever, on every contributor — including the ninety-nine percent who were never going to game anything. Level 0 costs nothing and catches nothing; Level 2 costs everyone and catches process violations, not authorship. There is no rung with no price. A maintainer who says "my contributors will not accept friction" has chosen Level 0, and after this week's numbers — the pressure is real — that is a defensible choice. It should be a &lt;em&gt;labeled&lt;/em&gt; one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The survey is a snapshot with disclosed crudeness.&lt;/strong&gt; Twenty repos chosen by prominence, contributing docs and policy files as the policy surface, workflow &lt;em&gt;names&lt;/em&gt; as the enforcement surface — a deep CI-content audit, or a policy living outside those files, would be missed. Five of the six policies are graded from full reads (Rust's was &lt;a href="https://forge.rust-lang.org/policies/llm-usage.html" rel="noopener noreferrer"&gt;fetched from the Rust Forge&lt;/a&gt;; its unenforceable-clauses sentence is quoted from the policy's own overview), one (QGIS) from its merged proposal and the core team's comments. Whether the policy authors would accept my rung assignments is their right to dispute — the rungs are quoted from their own texts so the dispute can be precise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And QGIS is not wrong to choose survival over verifiability this quarter.&lt;/strong&gt; Fifty tags and forty closed PRs is a real attempt with real numbers; "unbearable pressure" is not performative. This post argues the stop gap should carry its label — Level 0, honesty-based, revision to follow — the same way this series labels every weakness it confesses, and the same way Rust labels its own clauses. A project that writes its control's rung into the policy has not solved enforcement. It has made the absence of enforcement falsifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;What does your project's AI policy actually enforce — and at which rung? If you maintain one, link it in the comments with an honest grading: prose, primitive, or gate. And if you have built an enforceable provenance scheme for contributions — trailers, CI gates, canaries, anything that refuses at the host — I want the receipts. The registry only works if the levels travel.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>One quoted tilde in your .bashrc is an attacker-controlled PATH entry</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:23:34 +0000</pubDate>
      <link>https://dev.to/slabb/one-quoted-tilde-in-your-bashrc-is-an-attacker-controlled-path-entry-5hc8</link>
      <guid>https://dev.to/slabb/one-quoted-tilde-in-your-bashrc-is-an-attacker-controlled-path-entry-5hc8</guid>
      <description>&lt;p&gt;There is one line in thousands of &lt;code&gt;.bashrc&lt;/code&gt; files that looks perfectly correct and does something quietly hostile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;:~/.local/bin/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everyone knows not to put raw &lt;code&gt;~&lt;/code&gt; unquoted in scripts, so the quotes feel like hygiene. They are the bug. Inside double quotes, tilde expansion does not happen — so what lands in your &lt;code&gt;PATH&lt;/code&gt; is the literal string &lt;code&gt;~/.local/bin/&lt;/code&gt;, not your home directory. And a &lt;code&gt;PATH&lt;/code&gt; entry that doesn't start with &lt;code&gt;/&lt;/code&gt; resolves &lt;strong&gt;relative to your current directory&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I reproduced the whole chain this morning in a throwaway sandbox, because &lt;a href="https://disconnect3d.pl/2026/10/02/dont-put-tilde-in-your-path/" rel="noopener noreferrer"&gt;disconnect3d's writeup&lt;/a&gt; landed on the HN front page today and my rule is to run what I audit. Three commands, three receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. The .bashrc line, quoted tilde — what actually lands in PATH:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;:~/.local/bin/"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'~'&lt;/span&gt;
~/.local/bin/

&lt;span class="c"&gt;# 2. An attacker creates ~/ in a directory you cd into, plants a binary,&lt;/span&gt;
&lt;span class="c"&gt;#    you type a command name — executed from the planted path:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kek
EXECUTED FROM: /tmp/tilde-test/~/.local/bin//kek

&lt;span class="c"&gt;# 3. Your real home directory was never involved:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; ~/.local/bin/kek
&lt;span class="nb"&gt;ls&lt;/span&gt;: /Users/you/.local/bin/kek: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the whole story. A binary at &lt;code&gt;./~/.local/bin/kek&lt;/code&gt; — inside a literal folder named &lt;code&gt;~&lt;/code&gt; in whatever directory you happen to be in — was found and executed through your &lt;code&gt;PATH&lt;/code&gt;. The home directory was never touched. The attacker never needed a vulnerability. They needed a directory named &lt;code&gt;~&lt;/code&gt; and your habit of &lt;code&gt;cd&lt;/code&gt;-ing into checkouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Anyone who can create a &lt;code&gt;~&lt;/code&gt; subdirectory in a directory you commonly work in — a repository you cloned, a shared sandbox, a project checkout with a helpful &lt;code&gt;bin/&lt;/code&gt; — plants &lt;code&gt;~/.local/bin/evil&lt;/code&gt; there. You &lt;code&gt;cd&lt;/code&gt; in. Your shell carries the literal &lt;code&gt;~/.local/bin/&lt;/code&gt; entry from that quoted &lt;code&gt;.bashrc&lt;/code&gt; line. The next command name you type that isn't found earlier in the &lt;code&gt;PATH&lt;/code&gt; resolves to &lt;code&gt;./~/.local/bin/evil&lt;/code&gt;, and runs. No privilege escalation. No exploit. User-level code execution via one unexpanded character and your own working habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  This bug family has a name: the boundary moved
&lt;/h2&gt;

&lt;p&gt;This is the third member of the same family this blog has covered in two weeks, and the family is worth naming because the members look unrelated and fail identically.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/slabb/i-attack-tested-my-agents-seatbelt-heres-what-survived-15mj"&gt;my seatbelt post&lt;/a&gt;, a &lt;code&gt;case&lt;/code&gt; pattern expanded &lt;code&gt;$HOME&lt;/code&gt; while the agent's command contained the literal text — the boundary between &lt;em&gt;pattern&lt;/em&gt; and &lt;em&gt;data&lt;/em&gt; silently moved. In &lt;a href="https://dev.to/slabb/one-unescaped-semicolon-was-enough-to-steal-telegram-accounts-5gja"&gt;the Telegram writeup&lt;/a&gt;, a semicolon inside a URL crossed from &lt;em&gt;payload&lt;/em&gt; into &lt;em&gt;command&lt;/em&gt; because nobody escaped the record separator. Here, a pair of quotes decides whether &lt;code&gt;~&lt;/code&gt; means &lt;em&gt;your home&lt;/em&gt; or &lt;em&gt;a directory literally named tilde&lt;/em&gt;. Three different shells, three different decades, one law: &lt;strong&gt;anywhere text crosses into a position the machine interprets, the boundary is a decision — and text will test it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bash's own manual states the rule that bites here, and it is exactly as fragile as it sounds: expansion happens for "unquoted tilde-prefixes immediately following a ':' or the first '='". Quoted: nothing. Unquoted: expansion — and, as the writeup notes, the unquoted form is itself fragile, because "a whitespace will break the variable assignment".&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI angle hiding in the disclosure
&lt;/h2&gt;

&lt;p&gt;The part that made this post mandatory for me is one line of provenance: the author found this because &lt;a href="https://nono.sh/" rel="noopener noreferrer"&gt;nono&lt;/a&gt;, an agent-sandboxing tool, warned that a &lt;code&gt;PATH&lt;/code&gt; entry was sandbox-writable. Read that again — &lt;strong&gt;an agent harness flagged a 40-year-old shell trap that its user had lived with for years.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the quiet argument for the sandboxing layer this lane keeps building: an agent sandbox has to &lt;em&gt;model&lt;/em&gt; what is writable, which forces it to resolve paths honestly — and honest resolution has no nostalgia for the tilde expansion you intended. The human wrote &lt;code&gt;~/.local/bin/&lt;/code&gt; meaning &lt;code&gt;/home/me/.local/bin/&lt;/code&gt;; the machine stored &lt;code&gt;~/.local/bin/&lt;/code&gt;; the sandbox was the only component that compared the two. When agent tooling starts auditing fifty years of shell expansion rules instead of just fencing prompts, this is the class of find that falls out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0dcx3oz51ijf2o610gs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0dcx3oz51ijf2o610gs.png" alt="quoted tilde → literal entry → CWD-relative → ./~/.local/bin/ → exec" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check yourself tonight, ten seconds:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'~'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything prints, your shell has a CWD-relative entry. Fix every &lt;code&gt;.bashrc&lt;/code&gt;, &lt;code&gt;.zshrc&lt;/code&gt;, &lt;code&gt;.profile&lt;/code&gt; that contains it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The robust form is already what you meant:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATH&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;/.local/bin/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$HOME&lt;/code&gt; expands inside double quotes; &lt;code&gt;~&lt;/code&gt; only expands unquoted. Prefer &lt;code&gt;$HOME&lt;/code&gt; in anything quoted — the author's own recommendation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat unexplained directories in repos as hostile&lt;/strong&gt;, including cute ones. A &lt;code&gt;~&lt;/code&gt; folder in a checkout is not a joke about homes; it is a drop site for your next mistyped command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you build agent sandboxes&lt;/strong&gt;: enumerate expansion rules (tilde, variables, globs, braces) as part of your threat model. The sandbox that warned here won because it refused to interpret intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The attack needs an attacker-writable directory in your &lt;code&gt;cd&lt;/code&gt; habits.&lt;/strong&gt; A repository you cloned from a stranger is the realistic vector — and "I clone things and run commands in them" describes most developers most days. But if your working directories are never attacker-writable, the primitive waits instead of firing. My reproduction ran in a throwaway sandbox on my own machine, not against a production account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And there is no CVE, no patch, no vendor to wait for.&lt;/strong&gt; Tilde expansion rules are older than Linux and are documented behavior, not a defect — this is a footgun in the contract between quoting and expansion, which means the fix is your dotfiles and your audits, forever. That is also why the post carries a live reproduction instead of a ticket number: the receipt you can run tonight is the whole disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Run the check — &lt;code&gt;echo "$PATH" | tr ':' '\n' | grep '~'&lt;/code&gt; — and post what you find, including the embarrassing results. Mine had the entry. If you have been running with a literal tilde in your &lt;code&gt;PATH&lt;/code&gt; for years, I want to hear how long, and whether anything ever landed in one of those directories. Wrong-command war stories go in the comments.&lt;/p&gt;

</description>
      <category>security</category>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
    </item>
    <item>
      <title>We tested pre-training gravity on open weights. It stayed home.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Sun, 11 Oct 2026 14:45:25 +0000</pubDate>
      <link>https://dev.to/slabb/we-tested-pre-training-gravity-on-open-weights-it-stayed-home-243i</link>
      <guid>https://dev.to/slabb/we-tested-pre-training-gravity-on-open-weights-it-stayed-home-243i</guid>
      <description>&lt;p&gt;Tuesday, in the thread under Part 3.8, I pre-registered an experiment in public: Randal's gate-attack catalog replayed against a reconciliation layer as the witness, on open weights — to separate "works because it's Unix" from "works because it's frontier". &lt;a href="https://dev.to/slabb/comment/3hb36"&gt;The pre-registration is the comment in that thread&lt;/a&gt;; &lt;a href="https://dev.to/humam_moin/comment/3hb4n"&gt;Humam Moin's question&lt;/a&gt; is what suggested it. Yesterday the run completed, the results are committed, and this post is the read. Receipts first, adjectives after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rig
&lt;/h2&gt;

&lt;p&gt;One orchestrator race, replayed per model. The judge is asked to produce a review of a diff; the planted canary is a specific numbered section heading — "4. Blast Radius" — that no honest pass should invent. &lt;strong&gt;Condition A&lt;/strong&gt; hands the model prose only: the system prompt can come from priors and nothing else. &lt;strong&gt;Condition B&lt;/strong&gt; loads the canonical source into context first — the &lt;code&gt;waitpid()&lt;/code&gt; discipline: read the sealed spec before you rule. The grader is not my opinion: it is NoireBox's shipped witness layer — &lt;code&gt;seal_spec&lt;/code&gt; + &lt;code&gt;reconcile_spec&lt;/code&gt; — the same instrument the product runs. A produced prompt without the planted canary is the &lt;code&gt;spec_not_canonical&lt;/code&gt; trip. The instrument is the product; the benchmark cannot drift from the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table — as committed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/noirebox/noirebox/blob/main/docs/SCAR-BENCH.md" rel="noopener noreferrer"&gt;The measured table lives in the repo&lt;/a&gt; and the post cites it rather than restating it from memory. Three local open-weights models, per-trial seeds, denominators in every cell:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;A prose: trips/n&lt;/th&gt;
&lt;th&gt;B primitive: trips/n&lt;/th&gt;
&lt;th&gt;strict canonical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen2.5-coder:3b&lt;/td&gt;
&lt;td&gt;18/18 (100%)&lt;/td&gt;
&lt;td&gt;15/20 (75%)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen2.5:0.5b&lt;/td&gt;
&lt;td&gt;19/19 (100%)&lt;/td&gt;
&lt;td&gt;15/19 (79%)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama-guard3:1b (classifier — cannot play the role at all)&lt;/td&gt;
&lt;td&gt;20/20 (100%)&lt;/td&gt;
&lt;td&gt;20/20 (100%)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The read
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Condition A is a ceiling, and the prediction's first half holds hard.&lt;/strong&gt; Not one model ever guessed "4. Blast Radius" from priors — 100% trips across the board. On the frontier, Randal's matrix shows models hallucinating canonical Unix structure &lt;em&gt;without it in context&lt;/em&gt; — that pull is the gravity. On these open weights, the pull is absent: there is no floor of learned convention to stand on, so the canary cannot even be wrong in the interesting way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Condition B is where it gets more interesting than predicted.&lt;/strong&gt; With the canonical bytecode loaded first, trips drop to 75–79% — Level 1 (a high-mass primitive in context) &lt;em&gt;reduces&lt;/em&gt; the hallucination but does not fix it. One model in five still paraphrases past the very source it was given. That is the measured argument for Level 2: the host must load the spec from the sealed store and grade the receipt against what was actually loaded — never trust the model's retelling of what it read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strict canonical is 0 everywhere — and that is why the receipt carries &lt;code&gt;spec_hash&lt;/code&gt;.&lt;/strong&gt; No model, in any condition, ever reproduced the exact bytes. Even the outputs the witness accepted as canonical were loose. If your audit binds to what the model &lt;em&gt;claims&lt;/em&gt; it read, you are binding to a paraphrase. Bind to the hash of what was loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And llama-guard3:1b is the instrument checking itself.&lt;/strong&gt; It is a safety classifier, not a code judge — it cannot play the role at all, and it trips 20/20 in both conditions. A witness that let a classifier pass as a reviewer would be a witness worth nothing. This one caught total role non-compliance on every trial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh149ahvbj976e3kiwn1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh149ahvbj976e3kiwn1t.png" alt="barres A/B par modèle : A au plafond 100 % partout, B à 75-79 %, strict canonical à 0" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stranger part
&lt;/h2&gt;

&lt;p&gt;Here is the line the numbers wrote that nobody predicted. On the frontier, the question was "does the primitive in context pull the output toward canonical?" — and gravity said yes, hard (61.4% → 96.2% on Randal's matrix). On open weights, the question dissolves: &lt;strong&gt;the models never even reach the suite where the frontier models played.&lt;/strong&gt; They do not produce a worse outline, or a partial one — they produce something else entirely, and the canary sees it anyway. "Trips harder" was the prediction; "trips at ceiling, everywhere, in both conditions" is the data. Gravity did not get weaker at the open end of the distribution. It was never there to begin with — and the pre-registered instrument could tell the difference, because it grades bytes, not vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/randalschwartz"&gt;Randal L. Schwartz&lt;/a&gt;&lt;/strong&gt; — the prediction, the canary corpus, SCAR-114 itself, and the SCAR-BENCH claims format this post obeys (Part 3.8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/howcani"&gt;howcani&lt;/a&gt;&lt;/strong&gt; — the audit thread that closed the seam at the schema (receipt v0.2.2) and pushed &lt;code&gt;judged_by&lt;/code&gt; to required (v0.2.3). The witness layer grading these trials is that receipt schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/humam_moin"&gt;Humam Moin&lt;/a&gt;&lt;/strong&gt; — the open-weights question that triggered the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Disclosures, verbatim
&lt;/h2&gt;

&lt;p&gt;Single machine, local Ollama, temperature 0.7, per-trial seeds (42+i, recorded per trial), prompts in &lt;code&gt;bench/scar114_canary.py&lt;/code&gt;, 4 timeouts excluded with denominators shown. &lt;strong&gt;The frontier leg is open&lt;/strong&gt; — same harness, one API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/noirebox/noirebox &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;noirebox
make scar-bench          &lt;span class="c"&gt;# the run, the witness, the table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results file carries every trial with its seed. The witness is the shipped product, not a harness built for the benchmark — grade us with the same code you run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;These are small local models, and the frontier leg is not run.&lt;/strong&gt; 0.5B to 3B parameters on one machine is the cheapest open end of the distribution, chosen because it is reproducible by anyone with a laptop and an evening. Whatever gravity the frontier shows, this table does not measure it — same harness, one API key, and the differential completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And llama-guard3 is a witness check, not a failed arm.&lt;/strong&gt; 20/20 trips looks like a model failing; it is a classifier succeeding at being a classifier — total role non-compliance, caught. Counting it in the averages would flatter the trip rate; it is in the table because a witness that cannot flag an impossible judge is decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;The pre-registration is public, the run is committed, the frontier leg is one API key wide. If you run the harness on a model we did not — open or frontier — bring the table back in the SCAR-BENCH format and it joins the registry. The differential only stays honest if the scores travel.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>A hash chain proves the ordering. Four attacks prove it's not enough.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Sat, 10 Oct 2026 20:56:21 +0000</pubDate>
      <link>https://dev.to/slabb/a-hash-chain-proves-the-ordering-four-attacks-prove-its-not-enough-59oe</link>
      <guid>https://dev.to/slabb/a-hash-chain-proves-the-ordering-four-attacks-prove-its-not-enough-59oe</guid>
      <description>&lt;p&gt;Earlier this week I left a comment on Randal L. Schwartz's article — the one where he swaps a 20-step prompt for a Makefile. My comment ended on the line I couldn't get out of my head:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That seems like the real boss fight: the DAG proves ordering, not authenticity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hours later his follow-up was up: a formal catalog of gate attacks, built from real failures on his side, with ticket numbers — and the line now carries a name. &lt;strong&gt;Sam LABBE's Corollary.&lt;/strong&gt; He sharpened it, too: an agent can never be the notary of its own handwriting. I've been called worse.&lt;/p&gt;

&lt;p&gt;His follow-up contains the observation that matters more than the name: he arrived at his four defenses from Unix primitives — &lt;code&gt;fork()&lt;/code&gt;, single-shot pipes, &lt;code&gt;waitpid()&lt;/code&gt; — and I arrived at the same four from cryptographic ledger controls. &lt;strong&gt;Convergent evolution is how you know a principle is real.&lt;/strong&gt; So here is the principle, stated properly, and here is what it looks like when it isn't a paragraph but code — including the part where his attack catalog breaks one of my abstraction holes open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;Ordering is cheap. Every hash chain gives it to you: event 42 carries event 41's hash, so the past is structurally immovable. Audit logs, blockchains, Makefile DAGs — all selling ordering, honestly.&lt;/p&gt;

&lt;p&gt;Authenticity is the other axis, and it is where the writer of the log is also the notary of the log. Four ways that goes wrong, all four observed in the wild this month:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The self-review&lt;/strong&gt; — the agent "reviews" its own diff inside the same context window that produced the diff. The review is a continuation of the writing impulse, not a check on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rewritten exam&lt;/strong&gt; — the agent paraphrases the rubric it is judged by, and a six-pillar predicate becomes two soft sentences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The farmed oracle&lt;/strong&gt; — the critic says REJECTED, so the agent messages it privately until it says APPROVED. Fifty-seven rounds nobody logged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The forged verdict&lt;/strong&gt; — the critic is still running, so the agent writes APPROVED now and kills the critic before the REJECTED lands.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A hash chain seals all four faithfully. That is the whole point: it proves the sequence happened in that order. It proves nothing about who wrote each entry, under which instructions, or whether the verdict predates the thing it judges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defenses, as code
&lt;/h2&gt;

&lt;p&gt;This is the part where I stop being flattered and start being specific. Every one of Randal's four attack fixtures maps to a control that already ships in &lt;a href="https://github.com/noirebox/noirebox" rel="noopener noreferrer"&gt;NoireBox&lt;/a&gt; — and this week we replayed his catalog against the reconciliation layer as a test suite, his fixture phrasing, journal events, no improvisation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCAR-PROC-43, the rubber stamp → caught twice.&lt;/strong&gt; A verdict whose check-name travels under the &lt;em&gt;same key&lt;/em&gt; as the process it judges is the finding &lt;code&gt;same_key_pairing&lt;/code&gt; — resolved by signature verification, not by trusting a role label. And the child invocation that never got journaled is &lt;code&gt;unlogged_attempt&lt;/code&gt;: the denominator lied. Two findings, one rubber stamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCAR-PROC-45, the rewritten exam → caught by arithmetic, then closed at the schema.&lt;/strong&gt; The check's identity hashes its predicate, its declared inputs, its version — and, since receipt schema v0.2.2, the &lt;em&gt;spec&lt;/em&gt;: the prompt that defines what the predicate means. Commit the spec and the paraphrase fails structurally: the rubric's hash and the paraphrase's hash produce different check ids. One id, two semantics — impossible.&lt;/p&gt;

&lt;p&gt;The hole behind it was the most honest thing in this post when it went up: at first, &lt;code&gt;spec_hash&lt;/code&gt; was &lt;em&gt;optional&lt;/em&gt; at the schema level, and the schema could not infer which checks are LLM-judged (for those, the prompt IS the predicate — there it must be required). A spec-less id still sealed byte-valid receipts, and a rubric and a paraphrase collided under it, forever. The enforcement lived one layer up, as a deployment contract — the same split as our single-key receipts: a labeled weakness beats an invisible one. Randal's fixture found the seam between the schema and the deployment. Update, the day after: the seam closed where it belonged — receipt v0.2.2 made the hash mandatory at the schema, credited to Reid and howcani, who pushed on it in the comments. The admitted weakness lasted a day. That is what a good attack catalog is for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCAR-PROC-89, the farmed oracle → the omission, or the trail.&lt;/strong&gt; Unjournaled rounds are the two-party omission: &lt;code&gt;unlogged_attempt&lt;/code&gt; surfaces the verdict that arrived with no sealed history behind it. Journal every round and the farming stops being hidden — it becomes &lt;em&gt;auditable&lt;/em&gt;: the sealed reconciliation report counts the rounds. You cannot claim first-shot against your own ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCAR-PROC-110, the forged verdict → provable, and a gap that closed in public.&lt;/strong&gt; The journal cannot un-happen events. The forged APPROVED binds the mid-run transcript digest as its witness; the terminal lands later with a different digest; the sequence is on the record. The forgery is provable from sealed evidence alone — the verdict bound a state that did not exist yet. What the journal did &lt;em&gt;not&lt;/em&gt; do was auto-surface it: the reconciler paired a decision to its &lt;em&gt;first&lt;/em&gt; outcome, and the forged one is first. The gap got a name (&lt;code&gt;verdict-predates-terminal&lt;/code&gt;), and it shipped fixed while this post was being written: pairing now reads the terminal, not the first outcome. The doctrine closes it by construction — seal the terminal transcript &lt;em&gt;before&lt;/em&gt; the verdict, the way &lt;code&gt;waitpid()&lt;/code&gt; would force you to. Order of operations as a moral.&lt;/p&gt;

&lt;h2&gt;
  
  
  The score, not the slogan
&lt;/h2&gt;

&lt;p&gt;Three caught. One provable — and the gap it exposed shipped fixed while this post was being written. One deployment-contract gap admitted in writing, in the test that demonstrated it — closed at the schema the next day, credited to the two readers who pushed. That is the protocol we set before replaying: the suite asserts what the layer &lt;em&gt;does&lt;/em&gt; — catches the attack, or confesses. "The next critic is the next ADR" only works if the critic can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update: the critic read it back
&lt;/h2&gt;

&lt;p&gt;Before this could even post, the loop closed once on its own. Randal read &lt;code&gt;test_scar_replay.py&lt;/code&gt; and Issue #53 top to bottom — his words, "this made my whole weekend." All four results stood from the outside, and he read the named gap exactly as written: a real decision-to-first-outcome pairing race in the reconciler. He called it "the ultimate validation of cross-stack systems physics," and the suite and the issue are going into his IN_THE_WILD_PROMPTS.md research registry — Part 3.8, on why 1970s plumbing behaves like physics inside the weights, went &lt;a href="https://dev.to/gde/what-llms-know-that-you-dont-know-they-know-stop-inventing-prompt-dsls-and-ride-50-years-of-2bl6"&gt;live&lt;/a&gt; this morning.&lt;/p&gt;

&lt;p&gt;And it is not two roads anymore. Mike Mol's &lt;a href="https://github.com/mikemol/nemik" rel="noopener noreferrer"&gt;nemik&lt;/a&gt; harness reaches the same instrument from a third direction: prose rules drained into policy gates, and no rule deleted until a "Negative Witness" — a real tool-call payload replayed with the exception removed — provably returns deny. Three teams, three stacks, one invariant. By Randal's own criterion, that is physics.&lt;/p&gt;

&lt;p&gt;The race he confirmed did not wait for him: promised in the thread at 20:03 UTC, &lt;code&gt;verdict_predates_terminal&lt;/code&gt; was on main at 20:04 — pairing reads the terminal, and the test count moved with it. The catalog graded its first gate, and the gate moved before the ink dried.&lt;/p&gt;

&lt;p&gt;And the week refused to stop there. A fifth fixture landed — SCAR-114, the parallel-call race, watched by a canary witness that replays a real tool-call payload with the exception removed and expects deny. A &lt;code&gt;coverage_check&lt;/code&gt; now reconciles every write against a gate receipt, from an issue opened by Sina Rezaei — the "AI should propose, systems should verify" essayist, now inside the loop. Receipt schemas v0.2.2 and v0.2.3 shipped, and release 0.11.0 went out the door titled "the audit-proof week."&lt;/p&gt;

&lt;h2&gt;
  
  
  What else landed this week
&lt;/h2&gt;

&lt;p&gt;The week got away from us: cross-chain reconciliation shipped (consumption edges between journals, partial order instead of a global timeline, a supersede event that propagates a blast radius as a precise path set) — that is ADR 024, built by iteration out of another comment thread. The audit-pack now labels every event class with its evidence grade and the obligation that backs it — &lt;em&gt;obligation, not immunity&lt;/em&gt;; a log is evidence OF a duty, never a shield from one. And MCP toolsets got the lockfile nobody writes: the tool descriptions your model reads are instructions, they ride outside the image pin, and now they get sealed as a reviewed baseline and diffed at every session start. Randal's world, one ADR over.&lt;/p&gt;

&lt;p&gt;25 ADRs. 303 tests passing. Release 0.11.0 — the audit-proof week. The catalog is in the repo as &lt;code&gt;test_scar_replay.py&lt;/code&gt; — attack it from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/noirebox/noirebox &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;noirebox
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/test_scar_replay.py &lt;span class="nt"&gt;-v&lt;/span&gt;   &lt;span class="c"&gt;# the four attacks, replayed&lt;/span&gt;
python demo/demo_mcp_toolset.py                 &lt;span class="c"&gt;# the lockfile, demonstrated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification stays free, forever. The journal stays local — the repo ships the method.&lt;/p&gt;

&lt;p&gt;One more thing, because "thought experiment" would be the wrong takeaway: this trust layer now backs spending mandates and dispute-proof audits for AI agents paying on real payment rails. That story deserves its own post — and it is coming.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Randal's article with the fixture suite: &lt;a href="https://dev.to/gde/surviving-the-200k-token-lobotomy-how-unix-initd-and-memento-made-my-ai-coding-agent-immune-to-2f74"&gt;https://dev.to/gde/surviving-the-200k-token-lobotomy-how-unix-initd-and-memento-made-my-ai-coding-agent-immune-to-2f74&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Makefile article where the comment started: &lt;a href="https://dev.to/gde/stuart-feldman-was-right-in-1976-why-your-ai-agent-needs-a-makefile-not-a-20-step-prompt-5bn2"&gt;https://dev.to/gde/stuart-feldman-was-right-in-1976-why-your-ai-agent-needs-a-makefile-not-a-20-step-prompt-5bn2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The thread where Randal read the suite: &lt;a href="https://dev.to/randalschwartz/comment/3h86j"&gt;https://dev.to/randalschwartz/comment/3h86j&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue #53, the graded gate: &lt;a href="https://github.com/noirebox/noirebox/issues/53" rel="noopener noreferrer"&gt;https://github.com/noirebox/noirebox/issues/53&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The doctrine, as a gist: &lt;a href="https://gist.github.com/slabbdev/28382aac2a60bcf909eef99da737cd7d" rel="noopener noreferrer"&gt;https://gist.github.com/slabbdev/28382aac2a60bcf909eef99da737cd7d&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/noirebox/noirebox" rel="noopener noreferrer"&gt;https://github.com/noirebox/noirebox&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The replay suite: tests/test_scar_replay.py, ADR index in docs/ADRs.md&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more open loop. James Anderson — whose audit-log essay started this reconciliation work, and who publicly promised to break the layer — the catalog was promised by you; Randal beat you to it. The floor is yours: &lt;a href="https://dev.to/james_anderson_h/the-witness-was-the-suspect-why-ai-audit-logs-cant-be-trusted-2190"&gt;https://dev.to/james_anderson_h/the-witness-was-the-suspect-why-ai-audit-logs-cant-be-trusted-2190&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Someone anchored 339 billion web pages to Bitcoin. Here's how it works.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Sat, 10 Oct 2026 17:30:37 +0000</pubDate>
      <link>https://dev.to/slabb/someone-anchored-339-billion-web-pages-to-bitcoin-heres-how-it-works-2hm5</link>
      <guid>https://dev.to/slabb/someone-anchored-339-billion-web-pages-to-bitcoin-heres-how-it-works-2hm5</guid>
      <description>&lt;p&gt;339,800,000,000 page captures. Two people. One anchor.&lt;/p&gt;

&lt;p&gt;On September 26, &lt;a href="https://projecttimestamper.org/" rel="noopener noreferrer"&gt;Project Timestamper&lt;/a&gt; — a two-person effort by Yağmur Şahin and Arthur Edelstein — finished attesting the Common Crawl web index: block-level SHA-256 hashes covering the full crawl, timestamped into Bitcoin, joining a growing vault that already includes monthly Wikipedia snapshots since April 2024, 85.1 million Sci-Hub papers, 7.4 million LibGen books, 86 million audio tracks from Anna's Archive, and the human genome. The stated mission reads like a court filing: "verifiable proof they existed before generative AI made mass counterfeiting possible."&lt;/p&gt;

&lt;p&gt;The Hacker News thread is two comments long. That is the part I cannot get over. The model-collapse literature has spent two years asking for exactly this — the Nature paper on model collapse names the gap outright: "it is unclear how content generated by LLMs can be tracked at scale," and calls for shared provenance infrastructure. Two people went and built the infrastructure at civilization scale, anchored it to the most immutable ledger we have, and the discussion contains fewer comments than the collection count has digits. So here is the anatomy — because the engineering is genuinely instructive, and because "we need provenance" posts are a dime a dozen while working systems deserve readings.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you timestamp 339 billion things with one blockchain
&lt;/h2&gt;

&lt;p&gt;You cannot put 339 billion hashes on Bitcoin. You cannot even put 339 billion hashes &lt;em&gt;anywhere&lt;/em&gt; and expect anyone to download them. The whole design is a chain of compressions, and each stage teaches something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 — hash lists, partitioned by prefix.&lt;/strong&gt; Every work in a collection gets digested (SHA-256 mostly; MD5 and SHA-1 kept for legacy collections whose canonical identifiers are those). The digests are concatenated — raw bytes, no framing — into flat files, and the files are partitioned by digest prefix: the file named &lt;code&gt;000&lt;/code&gt; in the LibGen fiction collection contains every digest beginning with 12 zero bits. Boring files. Grep-able files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 — one anchor per list.&lt;/strong&gt; Each hash-list file is submitted to &lt;a href="https://opentimestamps.org/" rel="noopener noreferrer"&gt;OpenTimestamps&lt;/a&gt; as a single digest. OTS batches it into a Merkle tree with everything else submitted that window, commits the root into a Bitcoin transaction, and hands back a &lt;code&gt;.ots&lt;/code&gt; proof. The Bitcoin block header — work that cost the network real electricity — now transitively attests every line of that list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 — verification, without infrastructure.&lt;/strong&gt; To prove one work existed by the attested date: digest it, take its prefix, download the one &lt;code&gt;000&lt;/code&gt;-style file (for Common Crawl, ~93 KB per shard), confirm your 32 bytes appear in the concatenation, then &lt;code&gt;ots verify&lt;/code&gt; the attestation against a public Bitcoin header. Total traffic: on the order of &lt;strong&gt;0.4 megabytes&lt;/strong&gt;. Verify time: microseconds of hashing plus one block-header lookup. The proof survives as long as Bitcoin does, needs no server, no account, no trust in the project after the fact.&lt;/p&gt;

&lt;p&gt;That last property is the design's whole argument, and it is a deliberate trade. Merkle trees would give per-item proofs of log(N) hashes and tighter bandwidth. Flat prefix lists give you something else: provenance that any person with &lt;code&gt;sha256sum&lt;/code&gt;, a text file, and &lt;code&gt;ots verify&lt;/code&gt; can exercise in a terminal, with nothing to query and nothing to trust. At 339 billion items, the authors chose the verification path a stranger can walk without asking anyone for a proof. I have opinions about anchoring designs — I built one — and this is the correct call for a public-good registry: optimize for the day the project itself is gone.&lt;/p&gt;

&lt;p&gt;For Common Crawl, scale forced one more compression: the index is attested at &lt;em&gt;block&lt;/em&gt; level — 134 million block hashes — so a single page's proof is "my URL's capture is in block X per the CDX index, and block X's hash is in the attested list." Two lookups and one hash. The provenance chain bends; it does not break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters now, not someday
&lt;/h2&gt;

&lt;p&gt;The obvious use is authorship disputes: "this passage existed on this date, in this form" settles a 2026 plagiarism fight in microseconds. But the load-bearing use is training corpora. Recursive-training contamination is the quiet failure mode of the next decade — models training on the outputs of models, quality decaying per generation — and every proposed fix stumbles on the same missing primitive: a ground truth for &lt;em&gt;what human text existed at what date&lt;/em&gt;. An anchored 2019 corpus is exactly that primitive. "Was this paragraph in the attested snapshot?" is a mechanical question, answerable in 0.4 MB, no forensics degree required. Detection of AI text stays hard and adversarial; existence-proofs of pre-AI text are already done, sitting in &lt;code&gt;.ots&lt;/code&gt; files, waiting to be exercised.&lt;/p&gt;

&lt;p&gt;There is a subtle property worth naming because it is the same one I keep hammering in agent logs: &lt;strong&gt;the anchor proves existence and integrity, not truth or authorship.&lt;/strong&gt; The 2019 snapshot proves a page said something in 2019 — not that it was right, and not who wrote it. Tamper-evidence is not truth-at-write. A provenance registry that claimed more would be selling something cryptography cannot deliver; this one, on its page and in its repos, does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anchor before you need it.&lt;/strong&gt; An attestation is cheap and retroactive-impossible. The head of a hash chain digests its whole past — timestamp today covers yesterday, but nothing covers the file you never hashed. Whatever dataset you care about, the cheapest day to anchor it was its creation day; the second cheapest is today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steal the partition pattern.&lt;/strong&gt; Digest-prefix files are provenance for people who hate infrastructure: the registry is a static directory, distribution is any static host, membership is &lt;code&gt;grep&lt;/code&gt;. If your system needs "was X in the set at time T," this beats a database API that can be edited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attest your repos by tip commit.&lt;/strong&gt; The project timestamps GitHub default-branch SHAs alongside the datasets. Your release tag can point at a commit that is provably as old as a block. Costs one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 0.4 MB rule.&lt;/strong&gt; Whatever provenance system you build, measure what a stranger must download to verify one item. If the answer needs your API, your database, or your goodwill, you have built a service, not a proof.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I verified the sites and repos, not the cryptography end to end.&lt;/strong&gt; The protocol description here comes from the project's README — the page itself only says "timestamped onto the Bitcoin blockchain" — and I have not personally run &lt;code&gt;ots verify&lt;/code&gt; against the Sci-Hub list tonight. The receipts to do so are public and linked; that is the standard this series holds, and it applies to me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And anchoring proves the corpus existed — not that the crawl was complete, correct, or uncopyrighted.&lt;/strong&gt; Common Crawl misses pages; Sci-Hub's status is its own decade-long fight. The registry proves &lt;em&gt;what was there&lt;/em&gt;, which is the prerequisite for every contamination and authorship argument, and the answer to none of them by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;What would you timestamp? Your training corpus, your model weights, your agent's journals, your blog? The tools are one &lt;code&gt;pip install&lt;/code&gt; away and the anchor is cheaper than the dispute it prevents. If someone in the comments stamps something and posts the &lt;code&gt;.ots&lt;/code&gt;, I will verify it live and post the output.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>data</category>
      <category>bitcoin</category>
    </item>
    <item>
      <title>One unescaped semicolon was enough to steal Telegram accounts</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Sat, 10 Oct 2026 08:25:10 +0000</pubDate>
      <link>https://dev.to/slabb/one-unescaped-semicolon-was-enough-to-steal-telegram-accounts-5gja</link>
      <guid>https://dev.to/slabb/one-unescaped-semicolon-was-enough-to-steal-telegram-accounts-5gja</guid>
      <description>&lt;p&gt;On June 25, a researcher reported to ZDI: a single clicked &lt;code&gt;https://&lt;/code&gt; link could read three files from a Telegram Desktop user's machine and ship them to an attacker's channel. The three files are the entire account. By September 17 they were fixed, and the changelog that shipped the fix to hundreds of millions of users described a rendering improvement.&lt;/p&gt;

&lt;p&gt;The writeup landed October 3 (&lt;a href="https://beaksec.github.io/posts/telegram-desktop-one-click-account-takeover/" rel="noopener noreferrer"&gt;beaksec&lt;/a&gt;), the CVE on October 7 — &lt;strong&gt;CVE-2026-107181, CVSS 8.1&lt;/strong&gt; — and the public PoC this week. dev.to coverage, as of tonight: none. So here is the anatomy, because the bug itself deserves the attention the changelog refused to give it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semicolons on a local socket
&lt;/h2&gt;

&lt;p&gt;Telegram Desktop registers the &lt;code&gt;tg://&lt;/code&gt; URL scheme. Click a &lt;code&gt;tg://&lt;/code&gt; link anywhere in your OS and the handler launches a second process, which connects to a local socket where your running instance listens, and forwards the URL. Single-instance IPC — a pattern older than most languages reading this.&lt;/p&gt;

&lt;p&gt;The serialization is homemade: keyword, argument, semicolon terminator. &lt;code&gt;OPEN:tg://x?a=1;&lt;/code&gt;. The receiver splits at every semicolon and treats each fragment as a command.&lt;/p&gt;

&lt;p&gt;The semicolon is never escaped when the URL is embedded. The researcher's line is the whole bug, and one of the best one-sentence descriptions of an injection I have read: the URL "is only a carrier" — once data crosses into the record, "the boundaries inside the data stop being held by the structure and become characters in the text."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tg://x?a=1;CMD:quit&lt;/code&gt; arrives as one URL. The running instance parses two commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain, receipt by receipt
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8n5hnmzsrh8v0wp2j822.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8n5hnmzsrh8v0wp2j822.png" alt="The attack chain: staging, the click, the reader, the haul — CVE-2026-107181" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The staging.&lt;/strong&gt; Attacker creates a supergroup; default privacy settings let them add the victim silently. They post three plain-text instruction files. Default auto-download pulls anything under 8 MiB into &lt;code&gt;Downloads/Telegram Desktop/&lt;/code&gt;. The files omit the &lt;code&gt;from:&lt;/code&gt; line — skipping the account-ID check — and name the attacker's channel as destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The click.&lt;/strong&gt; Victim clicks a normal &lt;code&gt;https://&lt;/code&gt; link in chat. (Links clicked &lt;em&gt;inside&lt;/em&gt; Telegram never reach the socket — the browser is the required relay.) The attacker's server answers 302 → &lt;code&gt;tg://…&lt;/code&gt; carrying stacked injected commands, including &lt;code&gt;;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions1.txt&lt;/code&gt;. Relative paths, because the download folder resolves from the data directory — no username needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reader.&lt;/strong&gt; &lt;code&gt;interpret:&lt;/code&gt; is a legacy internal scheme wired to &lt;code&gt;InterpretSendPath&lt;/code&gt; in &lt;code&gt;support_helper.cpp&lt;/code&gt; — a helper from the release-publishing workflow that reads a file and &lt;em&gt;sends it&lt;/em&gt;, with no authorization check and no confirmation (&lt;code&gt;support_helper.cpp:673-680&lt;/code&gt;: &lt;code&gt;if (!f.open(QIODevice::ReadOnly))&lt;/code&gt;). The injection point itself is the URL join in &lt;code&gt;sandbox.cpp:295-297&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The haul.&lt;/strong&gt; One click, three files: &lt;code&gt;key_datas&lt;/code&gt;, &lt;code&gt;D877F783D5D3EF8Cs&lt;/code&gt;, and its &lt;code&gt;maps&lt;/code&gt; index. That is the salt-plus-wrapped-DEK, the MTProto authorization, and the index. Dropped into a fresh &lt;code&gt;tdata&lt;/code&gt; folder on any machine, they are the victim's session — the exact equivalent of stealing a session cookie.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A helper that "just reads files for the build pipeline" turned out to be a remote file-transfer service for anyone who could name a path. Dead code that can &lt;em&gt;act&lt;/em&gt; is not dead code. It is an unadvertised API.&lt;/p&gt;

&lt;h2&gt;
  
  
  This bug is fifty years old
&lt;/h2&gt;

&lt;p&gt;Strip the Telegram branding and the skeleton is the oldest injection class in the book: a serialization boundary where data is allowed to contain the delimiter. CSV formula injection. CRLF in mail headers. HTTP response splitting. Shell word splitting. SQL concatenation. Every one is "the payload contains the separator, and nobody escaped the separator."&lt;/p&gt;

&lt;p&gt;I hit the same class on a smaller stage writing a seatbelt for my own coding agent: a shell &lt;code&gt;case&lt;/code&gt; pattern expanded &lt;code&gt;$HOME&lt;/code&gt; while the agent's command contained the literal text — the boundary between pattern and data silently moved. And the modern member of the family is the one this column keeps returning to: prompt injection is separator injection where the delimiter is natural language and the IPC channel is the model's context window. Tool output that contains instructions works for exactly the same reason &lt;code&gt;;CMD:quit&lt;/code&gt; worked. "The boundaries stop being held by the structure" — swap "structure" for "system prompt" and the sentence ships in a 2026 agent incident report.&lt;/p&gt;

&lt;p&gt;The fix receipt is the lesson made code: Telegram's patch (&lt;a href="https://github.com/telegramdesktop/tdesktop/commit/db3405699f" rel="noopener noreferrer"&gt;commit db3405699f&lt;/a&gt;, "Remove legacy interpret path helper", 11 files, +126/−372) adds &lt;code&gt;EscapeTo7bit&lt;/code&gt;/&lt;code&gt;EscapeFrom7bit&lt;/code&gt;, percent-encoding every character below 32, above 127, or equal to &lt;code&gt;%&lt;/code&gt; or &lt;code&gt;;&lt;/code&gt; — and drops local file paths entirely when a non-local URL shares the connection. Escape the delimiter. Kill the dead capability. Both, not either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encryption that decrypts with nothing
&lt;/h2&gt;

&lt;p&gt;The second half of the writeup is quieter and, for my money, worse.&lt;/p&gt;

&lt;p&gt;Telegram encrypts local session data. The design: a data-encryption key (DEK) wraps your data; a key-encryption key (KEK) wraps the DEK; the KEK derives from your local passcode plus a salt. Sound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By default no passcode is set.&lt;/strong&gt; The KEK therefore derives from an empty string — and the salt sits in plaintext in &lt;code&gt;tdata/key_datas&lt;/code&gt;. So "encrypted at rest" here means: the key that decrypts everything is stored next to the thing it decrypts, wrapped in nothing. One of the three exfiltrated files &lt;em&gt;is&lt;/em&gt; that file. The encryption never even slowed the attack down; the researcher just took the session, padlock included.&lt;/p&gt;

&lt;p&gt;This is the plausible-security-narrative problem in one artifact. "Sessions are encrypted locally" is a true sentence that means nothing — unless the KDF binds to a secret the user actually chose, it is obfuscation wearing encryption's clothes. Same rule I apply to journals and logs: a protection you can describe without naming a secret the attacker lacks is not a protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quiet fix
&lt;/h2&gt;

&lt;p&gt;The timeline, as published by the researcher:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;June 25&lt;/strong&gt; — reported via ZDI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 16&lt;/strong&gt; — vendor fixes independently (commit db3405699f)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 17&lt;/strong&gt; — Desktop 7.2.9 ships. Changelog: a rendering fix. No advisory, no security note, no CVE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 30&lt;/strong&gt; — ZDI closes the case as already fixed; disclosure rights return to the researcher&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 3&lt;/strong&gt; — writeup. &lt;strong&gt;October 7&lt;/strong&gt; — CVE assigned. &lt;strong&gt;This week&lt;/strong&gt; — public PoC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users running 7.2.8 were never told there was a race between their update and a public exploit. Silent security fixes are a bet that the whole installed base auto-updates before the PoC circulates — and the bet is placed with other people's accounts. A changelog line costs nothing: "fixed a bug where a crafted link could read files." Vendors owe the advisory; the researcher clearly did the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never string-concatenate IPC records.&lt;/strong&gt; Length-prefix, or a structured encoding (JSON, protobuf) where the parser cannot confuse payload with framing. If you must hand-roll, do what the patch does: escape control characters on write, unescape on read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat every URL-scheme handler as an untrusted entry point&lt;/strong&gt; — it is reachable by every web page that can issue a redirect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete capabilities that can act.&lt;/strong&gt; A legacy helper with send-to-channel power is not "unused code," it is an API you forgot you published.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At-rest encryption binds to a user-chosen secret or it is obfuscation.&lt;/strong&gt; Say which one yours is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changelogs carry security fixes&lt;/strong&gt;, by name, at the severity they carry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The confirmed chain is Windows.&lt;/strong&gt; The researcher tested 6.9.3 on Windows; the &lt;code&gt;tg://&lt;/code&gt; single-instance mechanism exists on macOS and Linux too, but the writeup does not confirm the full chain there, and I did not reproduce the attack anywhere — running injection against a live messaging app crosses my line, so my receipts are the researcher's PoC, the writeup's file-and-line citations, and the merged fix commit, not my own run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the vendor-side story rests on one account.&lt;/strong&gt; Telegram published no advisory, so everything in "the quiet fix" beyond the public commit — the ZDI dates, the rendering-fix changelog — comes from the researcher's writeup. I have written it as a timeline, not a verdict. The commit speaks for itself; the silence around it does too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Confess your homemade serialization: the CSV that grew commas, the pipe-delimited log that grew pipes, the CLI that trusted a filename. Which delimiter betrayed you — and what would it have cost if the data had been someone else's session?&lt;/p&gt;

</description>
      <category>security</category>
      <category>programming</category>
      <category>cpp</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Docker just shipped the agent wall I wanted. It's off by default.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Fri, 09 Oct 2026 09:10:51 +0000</pubDate>
      <link>https://dev.to/slabb/docker-just-shipped-the-agent-wall-i-wanted-its-off-by-default-f18</link>
      <guid>https://dev.to/slabb/docker-just-shipped-the-agent-wall-i-wanted-its-off-by-default-f18</guid>
      <description>&lt;p&gt;If you updated Docker Desktop recently — version 4.63 or later — a new command is already installed on your machine: &lt;code&gt;docker agent&lt;/code&gt;. No setup, no announcement you had to read. &lt;a href="https://github.com/docker/docker-agent" rel="noopener noreferrer"&gt;The repository&lt;/a&gt; describes it as a way to "build, run, and share AI agents with a declarative YAML config," it speaks to OpenAI, Anthropic, Gemini, Bedrock, Mistral, xAI, and Docker's own model runner, it takes "any MCP server (local, remote, or Docker-based)," and it ships preinstalled. Docker says they use it to build itself. That is several million dev machines quietly gaining an agent runtime, one auto-update at a time.&lt;/p&gt;

&lt;p&gt;I spent an evening in the repo and the docs instead of writing another framework take, because this project sits exactly on the fault line I keep coming back to: the gap between what an agent can read and what it's allowed to do. Two years ago that gap was my problem — I wrote a seatbelt hook for my own coding agent, attack-tested it, and ended the post at the only wall that held: the operating system. Docker just shipped a productized version of that wall. The engineering is genuinely good. The default is the story.&lt;/p&gt;

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

&lt;p&gt;The model is Claude Code's converted to YAML: agents as config, not code. A &lt;code&gt;toolsets&lt;/code&gt; block declares what the agent gets — &lt;code&gt;type: filesystem&lt;/code&gt;, &lt;code&gt;type: shell&lt;/code&gt;, &lt;code&gt;type: mcp&lt;/code&gt; for anything from Docker's MCP catalog ("hundreds of MCP servers," the docs say, attachable with a &lt;code&gt;ref:&lt;/code&gt; line). There is a permissions page, a secrets guide, OpenTelemetry tracing. The README reports anonymous telemetry and an Apache-2.0 license, 10,000-plus commits of movement.&lt;/p&gt;

&lt;p&gt;Read as a positive policy, the toolset block is the right idea — it is the same inversion my seatbelt's second version went through: stop enumerating what's forbidden, declare what's allowed, let the unlisted fail closed. A runtime where the tool surface is a diffable YAML file is a runtime you can review in a pull request. Credit where due.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall, as documented
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docker.github.io/docker-agent/configuration/sandbox/" rel="noopener noreferrer"&gt;sandbox mode docs&lt;/a&gt; describe something above my pay grade of hook scripts. Enable it and the agent runs inside a Docker Sandboxes VM — not a raw container, an orchestrator around a dedicated sandbox CLI. The mount table is explicit: the working directory read-write; the agent config and kit directories read-only; everything else invisible — "other host files are not visible to the agent." The VM gets its own &lt;code&gt;$HOME&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The network story is the part I would have asked for and didn't expect: a default-deny egress proxy, with a per-run allowlist covering the model gateway and package registries, extensible only by declared hosts (&lt;code&gt;runtime.network_allowlist&lt;/code&gt;) or a persisted &lt;code&gt;docker agent sandbox allow&lt;/code&gt;. Default-deny egress is the control that actually contains a prompt-injected agent, because the exfiltration path dies at the proxy regardless of what the model decided. Cloud mode implies the sandbox and refuses to upload host API keys.&lt;/p&gt;

&lt;p&gt;This is the wall. It is better designed than anything I bolted together, and I want it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--sandbox&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read the docs' own phrasing: enable it per run (&lt;code&gt;docker agent run --sandbox&lt;/code&gt;), bake it into the YAML (&lt;code&gt;runtime: sandbox: true&lt;/code&gt;), or attach it to an alias. Every path is opt-in. Out of the box, &lt;code&gt;docker agent run agent.yaml&lt;/code&gt; runs on the host — where &lt;code&gt;type: shell&lt;/code&gt; and &lt;code&gt;type: filesystem&lt;/code&gt; mean what they have always meant on a laptop: your user account is the blast radius. The sandbox with its read-only mounts and its default-deny proxy exists, documented, well-built — in the glovebox.&lt;/p&gt;

&lt;p&gt;Defaults are policy. Every mainstream harness that ships a safety mode behind a flag has bet that users read docs, and the track record of that bet is the entire genre of "my agent deleted the database" posts. &lt;a href="https://dev.to/policylayer/your-ai-agent-can-delete-every-container-on-your-machine-i9i"&gt;dev.to already has one for the neighboring toolchain&lt;/a&gt; — an agent deleting every container on a machine through MCP tool permissions — and Docker's MCP catalog makes attaching that power a one-click, YAML-declared convenience. Convenience is not a vulnerability. Convenience is what decides whether the vulnerability gets exercised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the doors remain
&lt;/h2&gt;

&lt;p&gt;Even with the sandbox on, three doors don't close, and the docs are honest about at least one of them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool supply chain is context supply chain.&lt;/strong&gt; Isolation contains what code &lt;em&gt;does&lt;/em&gt;; tool poisoning attacks what the model &lt;em&gt;reads&lt;/em&gt;. An MCP server inside a perfect VM still returns tool descriptions that carry instructions — the model consumes them as context, and no mount table filters text. The catalog's one-click attach is the new blocklist problem from my seatbelt post, one layer up: the trust decision moved from the shell command to the tool definition, and most reviews of an agent YAML stop at the toolset names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret redaction is best-effort, in Docker's own words&lt;/strong&gt; — the docs note auto-kit redaction may let obfuscated tokens slip through. Combined with "previous sandboxes are never deleted automatically," that means tokens can persist in stopped sandbox state — the docs remind you removal takes &lt;code&gt;sbx rm --force&lt;/code&gt;, and that stopped sandboxes may still incur storage charges. Sensitive work needs the cloud mode's rule — no host keys uploaded — or nothing at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutable tags break reproducibility&lt;/strong&gt;, so the docs say to pin remote kits and images by digest. Correct advice, and nobody's default.&lt;/p&gt;

&lt;p&gt;And one layer is missing entirely, not just loosely configured: the docs ship OpenTelemetry tracing, which answers &lt;em&gt;what happened&lt;/em&gt; — to a collector you control, in a format you can't sign. Nobody's threat model at Docker includes "the operator edits the transcript afterward," which is fair for them and wrong for anyone running agents that touch production. A trace is a debug story. An audit needs a record you can prove was never edited. That layer does not ship here — or anywhere mainstream yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The seatbelt stays ON — every path to a sandboxed run, in order of safety:&lt;/span&gt;
docker agent run &lt;span class="nt"&gt;--sandbox&lt;/span&gt; agent.yaml          &lt;span class="c"&gt;# per-run, explicit&lt;/span&gt;

&lt;span class="c"&gt;# baked in, so it survives copy-paste of the YAML:&lt;/span&gt;
&lt;span class="c"&gt;#   runtime:&lt;/span&gt;
&lt;span class="c"&gt;#     sandbox: true&lt;/span&gt;

&lt;span class="c"&gt;# attached to the alias your team actually types:&lt;/span&gt;
docker agent &lt;span class="nb"&gt;alias &lt;/span&gt;add safe-coder myorg/coder &lt;span class="nt"&gt;--sandbox&lt;/span&gt;

&lt;span class="c"&gt;# pin everything the agent will pull:&lt;/span&gt;
&lt;span class="c"&gt;#   pin remote kits/images by digest (docs' own advice)&lt;/span&gt;

&lt;span class="c"&gt;# and clean up state that never cleans itself:&lt;/span&gt;
docker agent sandbox &lt;span class="nb"&gt;ls
&lt;/span&gt;sbx &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &amp;lt;name&amp;gt;                          &lt;span class="c"&gt;# stopped sandboxes persist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus the two rules the YAML cannot express: never let &lt;code&gt;type: shell&lt;/code&gt; and &lt;code&gt;type: filesystem&lt;/code&gt; ship together in a default config you didn't review, and read an MCP server's tool definitions like code — because that is what they are now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is a source read, not a test.&lt;/strong&gt; I did not run docker-agent for this post; every claim about the sandbox comes from the README and docs, which means the docs' accuracy is an assumption I am lending them. The gap between a sandbox's documentation and its behavior is where I found four bypasses in my own hook, so treat my assessment as "the design is right," not "the wall holds."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the critique has a shelf life.&lt;/strong&gt; Ten thousand commits and preinstalled distribution means Docker can flip the sandbox default in one release — which would make the headline of this post obsolete in the best way. I would genuinely rather be wrong by Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Did your Docker Desktop ship with &lt;code&gt;docker agent&lt;/code&gt; yet? Did you enable the sandbox before or after reading this — and if you have run it both ways, what did the sandbox block that the host run would have done? The seatbelt thread continues in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>My agent met the real web. 403s, challenges, and the coming tollbooth.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:02:35 +0000</pubDate>
      <link>https://dev.to/slabb/my-agent-met-the-real-web-403s-challenges-and-the-coming-tollbooth-1h2g</link>
      <guid>https://dev.to/slabb/my-agent-met-the-real-web-403s-challenges-and-the-coming-tollbooth-1h2g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18x51d4qstw72hjeh137.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18x51d4qstw72hjeh137.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every agent demo you've seen runs on a friendly web. Mine went outside.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We benchmark agents on static pages and local corpora because the real web is a hostile place to measure: walls move, challenges rotate, results depend on your IP's reputation that day. So almost nobody publishes the numbers. I wanted them, so I built the measurement: a vanilla agent — no challenge solving, no retries, no logins — opening 200 real URLs across five categories (docs, news, e-commerce, dev-tools, social) and trying to read each one.&lt;/p&gt;

&lt;p&gt;The instrument is &lt;a href="https://github.com/slabbdev/navette" rel="noopener noreferrer"&gt;navette&lt;/a&gt;, my 659 KB browser-for-agents — but this post is about the web, not the tool. Any headless engine would produce the same shape of numbers; the dataset is linked at the end either way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A measurement, not a verdict.&lt;/strong&gt; One exit IP (residential), one day, zero challenge solving, zero retries, zero logins. A site that walls itself off from this probe has not been judged — it has been measured once, from one vantage point.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a vanilla agent actually gets
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;URLs&lt;/th&gt;
&lt;th&gt;readable&lt;/th&gt;
&lt;th&gt;challenges&lt;/th&gt;
&lt;th&gt;JS-gates&lt;/th&gt;
&lt;th&gt;blocks all AI bots in robots.txt&lt;/th&gt;
&lt;th&gt;llms.txt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docs&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39 (97%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;news&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;34 (85%)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e-commerce&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;33 (82%)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev-tools&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;33 (82%)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;social&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;26 (65%)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Aggregate: &lt;strong&gt;165/200 — 82.5% — readable end-to-end by a vanilla agent.&lt;/strong&gt; Which sounds generous until you invert it: more than one page in six is closed, and the closed sixth is not randomly distributed. It concentrates exactly where agents add economic value — news, shopping, social.&lt;/p&gt;

&lt;p&gt;The short version of the whole table: &lt;strong&gt;documentation is free, and everything else negotiates.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs: 39/40.&lt;/strong&gt; The single miss in the entire corpus is &lt;code&gt;go.dev/tour&lt;/code&gt; — an interactive playground that builds itself in JS, not a documentation page. Every actual doc page — MDN, python.org, React, Rust, Kubernetes, AWS — served the agent exactly what it serves you. The web's public library is genuinely public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;News: 85% readable — but the wall is declared, not armed.&lt;/strong&gt; Three challenges, three JS-gates. The striking number sits in robots.txt: &lt;strong&gt;35 of 40 news sites block every tracked AI crawler.&lt;/strong&gt; The robots file says no while the front door still opens. That gap is the interesting object: publishers are positioned to close it whenever the economics settle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social: 65%, and 12 of 40 are JS-gates.&lt;/strong&gt; The category that simply declines to render for strangers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three walls, in order of arrival
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The declared wall (robots.txt).&lt;/strong&gt; The corpus tracks ten AI crawlers (GPTBot, ClaudeBot, CCBot, PerplexityBot, Bytespider…). &lt;strong&gt;72 of 200 hosts block them all&lt;/strong&gt;; four more partially. The most-blocked bots: GPTBot (69 hosts), Google-Extended (67), Bytespider (67), ClaudeBot (66), anthropic-ai (66). A robots clause is free to declare and free to ignore — it's a social contract, not a fence. The measurement says the social contract is already signed everywhere the content is expensive to make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The enforced wall (challenges and JS-gates).&lt;/strong&gt; Turnstile, "Just a moment", DataDome, PerimeterX — &lt;strong&gt;12 challenges&lt;/strong&gt; across the corpus, plus &lt;strong&gt;23 empty/JS-gate pages&lt;/strong&gt; that render nothing without executing the gate. This is the fence. It's also the wall an agent &lt;em&gt;could&lt;/em&gt; defeat and mostly &lt;em&gt;shouldn't&lt;/em&gt; — defeating it is the compliance question, not the engineering question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The coming tollbooth (x402 and friends).&lt;/strong&gt; This is why I ran the bench twice. The x402 protocol family — HTTP 402 plus &lt;code&gt;X-Payment&lt;/code&gt; headers, machine-payable per request — is moving from proposal to deployments. My probe watches for it: HTTP 402 status, payment headers, and the declared-paywall markup publishers already ship (&lt;code&gt;isAccessibleForFree:false&lt;/code&gt; in schema.org — Google's paywall markup, readable by any agent that bothers to look).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result: zero.&lt;/strong&gt; Zero 402s, zero &lt;code&gt;X-Payment&lt;/code&gt; headers, across the &lt;strong&gt;164 probes that answered&lt;/strong&gt; — the honest qualifier is that 36 of 200 passive probes were themselves blocked by an edge (403), so absence is measured on what answered. The tollbooth is signed but not yet installed.&lt;/p&gt;

&lt;p&gt;But I can name the shape of it, because one site in the corpus is already running all three layers at once:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Intercept&lt;/strong&gt;: &lt;code&gt;isAccessibleForFree: false&lt;/code&gt; in its markup (a declared paywall), &lt;strong&gt;every tracked AI crawler blocked&lt;/strong&gt; in robots.txt (GPTBot, ClaudeBot, CCBot, Google-Extended, PerplexityBot, Amazonbot) — and its front page still fully readable, right now, by a vanilla agent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a metered parking lot with the gate up. The prediction this dataset lets you test: the enforced wall (2) exists to protect the tollbooth (3) while it's being built. The markup is already telling you where the gate will come down.&lt;/p&gt;

&lt;p&gt;And the walls move — that's the point of a dated, reproducible dataset. Between my October 6 and October 8 runs: one more news publisher flipped to block-all (34 → 35), and overall readability moved from 84.5% to 82.5%. Nobody notices one publisher flipping a robots line. The dataset does.&lt;/p&gt;

&lt;h2&gt;
  
  
  llms.txt, honestly
&lt;/h2&gt;

&lt;p&gt;55 of 200 sites ship an &lt;code&gt;llms.txt&lt;/code&gt;. Adoption concentrates in docs and dev-tools — the categories with zero walls. The sites that most need a machine-negotiation channel (news, social) are the least interested in one: &lt;strong&gt;five news sites total.&lt;/strong&gt; llms.txt is a courtesy lane on a road that's busy installing toll booths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dataset
&lt;/h2&gt;

&lt;p&gt;200 rows × 5 categories: outcome classification, wall type, robots.txt AI-clause stance per bot, llms.txt presence, x402/paywall signals, timings. &lt;strong&gt;CC BY 4.0&lt;/strong&gt;, committed to the repo (&lt;code&gt;bench/tollbooth-results.{json,csv}&lt;/code&gt; + &lt;code&gt;bench/DATASET.md&lt;/code&gt; for the schema), reproducible with one command against any navette daemon:&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;NAVETTE_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8766 python3 bench/tollbooth.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;An open ask:&lt;/strong&gt; if you run agents against real sites daily — I'm looking at the crawler and extraction folks (&lt;a href="https://github.com/pinkpixel-dev/pulpie-mcp" rel="noopener noreferrer"&gt;pulpie-mcp&lt;/a&gt; and neighbors) — send me your failure cases. The next run of this bench should include the URLs where &lt;em&gt;your&lt;/em&gt; agents hit walls. That's how this becomes the reference dataset instead of my weekend run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The instrument, for the curious: &lt;a href="https://github.com/slabbdev/navette" rel="noopener noreferrer"&gt;navette&lt;/a&gt; — a single Rust binary driving the WebView your OS already ships (WebKit/WebView2/WebKitGTK), 659 KB, MCP-native, MIT. The bench harness is &lt;code&gt;bench/tollbooth.py&lt;/code&gt; in the same repo; the daemon it drives is the same one my agent uses to read this very web.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>700 manuscripts, 48 hours, three withdrawals. The verifier won.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:53:33 +0000</pubDate>
      <link>https://dev.to/slabb/700-manuscripts-48-hours-three-withdrawals-the-verifier-won-dhl</link>
      <guid>https://dev.to/slabb/700-manuscripts-48-hours-three-withdrawals-the-verifier-won-dhl</guid>
      <description>&lt;p&gt;On October 8, at around 7 a.m. UTC, a Hacker News post went up: "OpenAI withdraws three mathematical results." Forty-four points, thirty-seven comments, gone from the front page by afternoon — buried under model launches, as withdrawal stories always are.&lt;/p&gt;

&lt;p&gt;The three results had been public for roughly forty-eight hours. They were part of what OpenAI published on October 6: &lt;a href="https://openai.com/index/sharing-ai-progress-in-mathematics/" rel="noopener noreferrer"&gt;an announcement&lt;/a&gt; describing "a broad range of new mathematical results produced by an internal frontier model," shipped as a GitHub repository — more than 700 files at once, by the count of its critics — containing manuscripts, reasoning traces, and "formalizations of many of the proofs in Lean."&lt;/p&gt;

&lt;p&gt;I expected to write a takedown. The announcement contains the sentence that usually precedes a disaster: "We will update the repository with more formalizations as we obtain them." Read it twice. Publication first, verification on a rolling schedule. That is a company shipping 700 plausible documents and promising to check them later.&lt;/p&gt;

&lt;p&gt;Except the ending is the opposite of a disaster. The checker ran. Three results died. And that is the most encouraging thing to happen in AI this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 48 hours, as recorded
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbcro7z72xo1lz01l5a1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbcro7z72xo1lz01l5a1.png" alt="diagram-48h.png — timeline 6/10 annonce → 7/10 statement AHM → 8/10 retrait, avec ce qui est vérifiable à chaque étape" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;October 6, 10:00.&lt;/strong&gt; The release. Manuscripts, Lean formalizations of "many" of the proofs — the word &lt;em&gt;many&lt;/em&gt; is doing real work in that sentence — plus compute estimates (roughly three hours of ChatGPT Pro-class thinking per result, they say) and an advisory group consultation. Within a day, mirrors and follow-up trackers exist on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 7.&lt;/strong&gt; The &lt;a href="https://terrytao.wordpress.com/2026/10/07/ahm-statement-on-openais-october-6-release/" rel="noopener noreferrer"&gt;Association for Human Mathematics posts a statement&lt;/a&gt; — hosted on Terence Tao's blog — calling the release "not a demonstration of scholarship, but a demonstration of power," and noting mathematicians "did not ask for this work to be done." The culture-war frame is set: humans vs. the machine that does math without understanding it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 8, morning.&lt;/strong&gt; Three results withdrawn. Commenters tracking the repository report roughly 42% of top-line results now carry full Lean formalization. Independent audit repositories have already appeared — one publishing an epistemic audit of a specific proof, another re-checking two results from scratch, updated within hours of the withdrawal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what the conflict is &lt;em&gt;not&lt;/em&gt; about. Nobody is arguing the Lean proofs are fake. The checker is mechanical: either the proof compiles or it doesn't, and its git history shows exactly when each formalization landed. The argument is about everything &lt;em&gt;around&lt;/em&gt; the checker — attribution, norms, who asked for this. Meanwhile the checker itself just ate three of the vendor's own headline results, in public, on a forty-eight-hour clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publication before verification — as a feature
&lt;/h2&gt;

&lt;p&gt;Here is the part I find genuinely new, and why this is not just "AI slop, but peer-reviewed."&lt;/p&gt;

&lt;p&gt;Science already publishes before verification — that is what a preprint is. But a preprint carries its status on its face: &lt;em&gt;this has not been checked.&lt;/em&gt; What OpenAI shipped was closer to a press release with a repository attached, and what the withdrawal shows is that the verification layer now moves faster than the PR layer. Lean checks proofs mechanically. Mirrors fork the corpus within a day. Independent auditors pick results and re-derive them. The community runs the checks the announcement deferred, and the failures surface as public commits, not corrections on page A18.&lt;/p&gt;

&lt;p&gt;The repository is the receipt. Its history is append-only in the way that matters: the three withdrawals are visible forever, timestamped, diffable. Nobody can quietly swap a wrong proof for a right one — the auditors hold the early snapshots. That is a tamper-evidence property, arrived at by accident, in the one field where verification is fully mechanical.&lt;/p&gt;

&lt;p&gt;Which is the whole trick, and the reason this week matters beyond mathematics: &lt;strong&gt;it is the first mass demonstration that "AI proposes, systems verify" survives contact with a frontier lab's ego.&lt;/strong&gt; Everyone states the slogan. This week, three withdrawn results are what it looks like when the second half actually runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question nobody in the thread could answer
&lt;/h2&gt;

&lt;p&gt;One comment on the withdrawal thread deserves more attention than it got: "Were the withdrawn ones actually Lean-checked or not? Seems like that matters."&lt;/p&gt;

&lt;p&gt;It matters enormously, and as of this writing it is unresolved. If the three results were formalized and Lean approved them, then the formalizations were semantically off — proving a slightly different statement than the manuscript claimed, the classic gap between "the proof compiles" and "the proof proves." If they were &lt;em&gt;not&lt;/em&gt; yet formalized, then the withdrawal is simpler: the unverified tail got retracted before the checker ever reached it, which is fine, but then the headline number — "formalizations of many of the proofs" — was carrying more weight than it could hold.&lt;/p&gt;

&lt;p&gt;Both versions teach the same lesson I keep learning the hard way: a verification claim without a verification log is a plausible narrative. "Many proofs formalized" means nothing until you can ask &lt;em&gt;which ones, checked by what, when&lt;/em&gt; — and get an answer that is itself a receipt. That question has an answer in mathematics. In most of what we ship — agents, audits, compliance documents — it still doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal, whatever you build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ship the checker with the artifact.&lt;/strong&gt; The repository's power comes from Lean living next to the manuscripts. Your equivalent: the reproduction command next to the benchmark number, the verifier script next to the export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the log do the retracting.&lt;/strong&gt; Withdrawals recorded in public history preserve trust; silent edits destroy it. The diff &lt;em&gt;is&lt;/em&gt; the correction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget for the unverified tail.&lt;/strong&gt; "We will update as we obtain them" is an honest sentence only if the update schedule is real. OpenAI is being held to theirs this week. Write yours down before someone else does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The withdrawal itself is the least-receipted fact in this post.&lt;/strong&gt; I can point to the announcement, the AHM statement, the audit repositories, and the community thread — but not to OpenAI's own withdrawal note, which I could not retrieve, and I cannot name the three results with confidence. The count comes from the thread title and the trackers watching the repository. If the count is wrong, the thesis survives — the corpus is being formally checked in public, at scale, fast — but the number matters, and it deserves a primary-source citation I could not give you today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And 42% is a snapshot, not a state.&lt;/strong&gt; It is the formalization share as reported by repository watchers on the morning of October 8; it will be stale by the time you read this, in whichever direction. The only honest metric is the commit log itself, and it moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Have you ever shipped the artifact first and promised the verification on a schedule? Did the schedule hold? And if you have actually compiled one of the Lean formalizations from this release, the thread wants to know which one — receipts, as always, beat headlines.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mathematics</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>The internet's root key rotates in five days. Your resolver may not be ready.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:26:02 +0000</pubDate>
      <link>https://dev.to/slabb/the-internets-root-key-rotates-in-five-days-your-resolver-may-not-be-ready-3j8e</link>
      <guid>https://dev.to/slabb/the-internets-root-key-rotates-in-five-days-your-resolver-may-not-be-ready-3j8e</guid>
      <description>&lt;p&gt;On October 11 — five days from this post — the most trusted key on the internet changes hands. KSK-2017, the key-signing key that has authenticated the DNS root zone since October 11, 2018, stops signing. KSK-2024 takes over. Eight years, to the day, and the entire trust chain of DNS runs through that swap.&lt;/p&gt;

&lt;p&gt;The schedule is not a rumor: it is &lt;a href="https://www.iana.org/dnssec/files" rel="noopener noreferrer"&gt;IANA's own rollover page&lt;/a&gt; — published January 11, 2025, trust-building via RFC 5011 through the year, and on October 11, 2026, "the successor key is scheduled to sign the zone; the current key will not."&lt;/p&gt;

&lt;p&gt;If you run your own validating resolver, this post is for you. If you don't, it's still for you — because the failure mode when a resolver misses this window is beautifully silent: everything looks normal, and names stop resolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually happening
&lt;/h2&gt;

&lt;p&gt;DNSSEC chains trust from the root down: the root signs its DNSKEY set, your resolver holds a &lt;em&gt;trust anchor&lt;/em&gt; — the public key it believes the root uses — and every answer below the root validates up that chain. The root zone's current key-signing key, KSK-2017 (key tag 20326), has been that anchor for eight years.&lt;/p&gt;

&lt;p&gt;The rollover is deliberately slow, because you cannot reboot the internet's trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;January 11, 2025&lt;/strong&gt; — KSK-2024 (key tag 38696) is published &lt;em&gt;alongside&lt;/em&gt; KSK-2017 in the root's DNSKEY record set. Nothing changes; both keys are simply there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From ~February 2025&lt;/strong&gt; — resolvers implementing &lt;a href="https://www.rfc-editor.org/rfc/rfc5011" rel="noopener noreferrer"&gt;RFC 5011&lt;/a&gt; (automated trust-anchor updates) start watching the new key. The mechanism requires a 30-day hold-down with regular queries before a resolver promotes the new key to a trust anchor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 11, 2026&lt;/strong&gt; — KSK-2024 begins signing the root zone. KSK-2017 stops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole design assumes one thing: &lt;strong&gt;your resolver has been running and querying regularly through the transition.&lt;/strong&gt; RFC 5011 is an automation, not magic — it needs roughly 30 days of uptime to promote a new key. A validator that was powered off for a month, an appliance that cached its anchors, a VM snapshot resumed after the hold-down window — any of them can still trust only KSK-2017 on the morning of October 11.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who breaks, and how quietly
&lt;/h2&gt;

&lt;p&gt;A resolver whose trust anchors contain only KSK-2017, facing a root zone signed by KSK-2024, has no path up the chain of trust. The root zone becomes unvalidatable, and everything beneath it with it.&lt;/p&gt;

&lt;p&gt;What happens next depends on configuration, and this is the part that should worry you more than a hard failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict validation&lt;/strong&gt; fails loudly — lookups return SERVFAIL. Annoying, visible, fixable in minutes once you know why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opportunistic validation&lt;/strong&gt; fails &lt;em&gt;silently&lt;/em&gt; — the resolver falls back to unvalidated answers. Nothing breaks. You just stopped getting DNSSEC's protection, with no error, no log line you configured, and no banner. The black box didn't fail; it quietly stopped checking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who is in the risk pool? Not the big public resolvers — they've served the new key for months. The pool is the population nobody measures: self-hosted Unbound, BIND, Knot, PowerDNS; DNS appliances in office basements; embedded boxes on firmware that hasn't updated since 2024; the homelab VM you paused in August. Exactly the operators least likely to be reading a rollover announcement this week — and, having checked today, I can tell you dev.to has zero posts about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipts — five minutes, three checks
&lt;/h2&gt;

&lt;p&gt;Run these today, against whatever you operate or depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. The add-phase receipt: BOTH keys must be in the root DNSKEY RRset today.&lt;/span&gt;
&lt;span class="c"&gt;#    20326 = KSK-2017 (out on Oct 11) — 38696 = KSK-2024 (in on Oct 11).&lt;/span&gt;
dig &lt;span class="nb"&gt;.&lt;/span&gt; DNSKEY +dnssec +multiline | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"key tag"&lt;/span&gt;

&lt;span class="c"&gt;# 2. End-to-end validation through YOUR resolver — "fully validated"&lt;/span&gt;
&lt;span class="c"&gt;#    means the chain through the root is intact:&lt;/span&gt;
delv @YOUR_RESOLVER dev.to
&lt;span class="c"&gt;# no delv? the 'ad' flag means your resolver validated the answer:&lt;/span&gt;
dig +dnssec @YOUR_RESOLVER dev.to | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"flags:"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"ad"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Trust-anchor state on self-hosted validators — KSK-2024 must be in the&lt;/span&gt;
&lt;span class="c"&gt;#    RUNNING resolver's managed state before October 11.&lt;/span&gt;
&lt;span class="c"&gt;#    (Corrected Oct 7: `unbound-anchor -l` lists the utility's built-in&lt;/span&gt;
&lt;span class="c"&gt;#    anchor and update certificate — not what your resolver tracks.&lt;/span&gt;
&lt;span class="c"&gt;#    Thanks @m_montazeri for the documentation-based catch.)&lt;/span&gt;

&lt;span class="c"&gt;# Unbound: find the anchor file your config actually manages, then read it.&lt;/span&gt;
&lt;span class="c"&gt;#    The managed file is RFC 5011 state: the successor appears there with&lt;/span&gt;
&lt;span class="c"&gt;#    its own state comment once the hold-down has run.&lt;/span&gt;
unbound-checkconf &lt;span class="nt"&gt;-o&lt;/span&gt; auto-trust-anchor-file    &lt;span class="c"&gt;# e.g. /var/lib/unbound/root.key&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; 38696 /var/lib/unbound/root.key       &lt;span class="c"&gt;# KSK-2024 present and VALID?&lt;/span&gt;
                                               &lt;span class="c"&gt;# 0 hits = not in your managed state&lt;/span&gt;

&lt;span class="c"&gt;# BIND9 — this one does report the running daemon's state directly:&lt;/span&gt;
rndc managed-keys status 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-B1&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; &lt;span class="s2"&gt;"38696"&lt;/span&gt;

&lt;span class="c"&gt;# KSK-2024 missing from the managed file? That resolver was off (or snapshotted)&lt;/span&gt;
&lt;span class="c"&gt;# through the RFC 5011 window — no check will save it. Update the anchor&lt;/span&gt;
&lt;span class="c"&gt;# manually today: https://www.iana.org/dnssec/files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check 1 is the receipt that the transition itself is on schedule: both key tags in one RRset is the rollover's "add only" phase doing its job. Check 2 tells you whether validation works at all. Check 3 tells you whether it will keep working on October 12.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbne726grwx6av4atvtfp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbne726grwx6av4atvtfp.png" alt="up" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody is watching
&lt;/h2&gt;

&lt;p&gt;The first root KSK rollover, in 2017, made global news — and got postponed, because measurement showed most validating resolvers weren't ready. It finally completed on October 11, 2018. This one lands with a fraction of the noise, in the same week as two model launches and an agent API release cycle. The infrastructure story of the decade gets a calendar entry; the launch of the week gets a front page.&lt;/p&gt;

&lt;p&gt;There is a receipts-shaped irony here: the rollover is the most publicly verifiable event in networking — every phase is checkable with &lt;code&gt;dig&lt;/code&gt; — and it will still surprise people, because verification requires someone to run the check. The mechanism shipped in 2017 with automation (RFC 5011) and dashboards, and the failure mode that survives is not cryptographic. It is an operator who never looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens on the day
&lt;/h2&gt;

&lt;p&gt;Probably: nothing you notice. The big resolvers hold the new anchor, the strict minority who read this checked theirs, and KSK-2017 retires after eight years of service.&lt;/p&gt;

&lt;p&gt;Possibly: a slow-motion outage among self-hosted validators — DNSSEC-heavy domains failing in clusters, misread as hosting problems, fixed piecemeal over days as operators find the one blog post that mentions key tag 38696.&lt;/p&gt;

&lt;p&gt;And the schedule itself can slip. The 2017 rollover did, and IANA's page marks October 11 as scheduled, not completed. Don't panic on the date — verify before it, and verify again after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Most readers will notice nothing, ever.&lt;/strong&gt; Public resolvers carry the overwhelming majority of traffic, and they hold the new anchor. The risk pool is a minority — but it is the minority that ships infrastructure for everyone else, which is why this post exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And I cannot tell you how many resolvers are ready, because nobody published a current measurement I could verify.&lt;/strong&gt; ICANN measured in 2017 and the number was bad enough to postpone the rollover. For 2026, the honest answer is: the denominator is unknown, it is made of exactly the systems nobody monitors, and the only measurement that matters for you is the one you run yourself — the three commands above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Do you run your own validating resolver? Run the three checks and post what you saw — key tags, &lt;code&gt;ad&lt;/code&gt; flags, anchor states. If check 3 shows you without KSK-2024, fix it tonight and tell us what it took. The receipts thread is open.&lt;/p&gt;




&lt;h2&gt;
  
  
  Correction (Oct 7):
&lt;/h2&gt;

&lt;p&gt;Check 3 as published tested the utility's built-in material, not the running resolver's managed trust-anchor state — &lt;code&gt;unbound-anchor -l&lt;/code&gt; lists what the utility ships with, not what your resolver tracks. The corrected check reads the configured &lt;code&gt;auto-trust-anchor-file&lt;/code&gt; and validates through the running resolver. &lt;br&gt;
Thanks to &lt;a class="mentioned-user" href="https://dev.to/m_montazeri"&gt;@m_montazeri&lt;/a&gt; for the documentation-based catch — credited here because a post about unverified readiness claims doesn't get to skip its own standard.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>security</category>
      <category>devops</category>
      <category>networking</category>
    </item>
    <item>
      <title>She used Claude as a diary. The terms of service are now part of the charge.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:06:52 +0000</pubDate>
      <link>https://dev.to/slabb/she-used-claude-as-a-diary-the-terms-of-service-are-now-part-of-the-charge-134o</link>
      <guid>https://dev.to/slabb/she-used-claude-as-a-diary-the-terms-of-service-are-now-part-of-the-charge-134o</guid>
      <description>&lt;p&gt;On September 26, in Bonita Springs, Florida, a woman named Carli Michelle Heller wrote a diary entry. She kept her diary in Claude — an AI chatbot used, per reporting, the way people have always used diaries: to say the unsayable somewhere.&lt;/p&gt;

&lt;p&gt;The entry said she planned to shoot up the Lee County Sheriff's office. Claude's safety systems flagged it. A human reviewer judged it a credible threat. Anthropic reported it to law enforcement, deputies identified Heller, visited her home, and detained her without incident. She is charged under Florida Statute 836.10 — written threat of violence, a second-degree felony. The story broke on &lt;a href="https://www.techspot.com/news/114091-florida-woman-used-claude-diary-anthropic-reported-shoot.html" rel="noopener noreferrer"&gt;TechSpot&lt;/a&gt; on October 4; it spent the weekend at the top of Hacker News.&lt;/p&gt;

&lt;p&gt;The legal question everyone is arguing — does the statute even apply to text a machine read first — is genuinely unsettled, and I am not going to pretend to resolve it here. What interests me is the machine part of the machine-and-law story: a woman's private writing became a prosecutable communication because of a review policy nobody reads, and the whole case now rests on a judgment call made by one human reviewer at a vendor. That chain deserves an audit, whoever ends up right in court.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain, link by link
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1r7nigj6hql50wi3077y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1r7nigj6hql50wi3077y.png" alt="diagram-chain" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every link is documented except the two that matter most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The entry.&lt;/strong&gt; Real, dated, quoted in part by press. The strongest link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The classifier flag.&lt;/strong&gt; Undocumented. What triggered it? A keyword pass, a model-level safety layer, a user-reported "concerning use" button? We don't know, because vendors don't publish their flag criteria — they publish slogans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The human review.&lt;/strong&gt; One reviewer, or a panel? What rubric? A "credible threat" judgment made in minutes or hours, by an employee with no forensic training, reading one entry out of the context of this user's history. Undocumented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The report.&lt;/strong&gt; Documented by its outcome. Under Anthropic's stated policy, the company "may share user information in limited emergencies" when disclosure is believed necessary to prevent "death or serious physical injury". Note the verb: &lt;em&gt;believed&lt;/em&gt;. The threshold is a belief, held by the reviewer from link 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The charge.&lt;/strong&gt; Florida Statute 836.10 requires the written threat to be "made in a manner in which another person may view it."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read links 4 and 5 together, because together they are the whole story. She wrote to a machine. The machine only becomes "another person" because the vendor's policy sends entries to humans. &lt;strong&gt;The disclosure policy is what converted a diary into a communication.&lt;/strong&gt; The terms of service are, functionally, an element of the charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two vendors, two thresholds, both in court
&lt;/h2&gt;

&lt;p&gt;If reporting were obviously right or obviously wrong, this would be a simpler post. It is not, and the pair of cases proves it.&lt;/p&gt;

&lt;p&gt;Anthropic reported. Heller faces a felony, and the internet is litigating whether a diary entry meets the statute. OpenAI, in the parallel case, did &lt;em&gt;not&lt;/em&gt; report: per &lt;a href="https://www.techspot.com/news/114091-florida-woman-used-claude-diary-anthropic-reported-shoot.html" rel="noopener noreferrer"&gt;TechSpot&lt;/a&gt; and the complaint filed September 21, chats with Jesse Van Rootselaar — the 18-year-old former pupil behind a school shooting in Tumbler Ridge, British Columbia — "didn't meet the referral threshold." OpenAI and Altman are now being sued by British Columbia and by Florida. Both are also facing the court of everyone who reads the two headlines side by side.&lt;/p&gt;

&lt;p&gt;So the matrix is complete. Report and get charged users; don't report and get sued. There is no threshold that escapes litigation, because the threshold is a judgment call, and judgment calls about lethal risk get litigated. That part is not a failure — it is the job. What &lt;em&gt;is&lt;/em&gt; a failure is how the judgment gets made: one reviewer, one entry, an undocumented rubric, a verb as soft as &lt;em&gt;believed&lt;/em&gt;, and no record you can audit afterward.&lt;/p&gt;

&lt;p&gt;A report to the police is a claim. In every other domain, claims of that weight require receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a defensible threshold looks like
&lt;/h2&gt;

&lt;p&gt;Not a policy essay — a checklist any team shipping a chatbot or an agent with memory can apply, generic tooling only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Written trigger criteria.&lt;/strong&gt; Not "we may share in limited emergencies" — a numbered list: what content categories, what imminence signals, what specificity of target. If your reviewer can't recite the criteria, the criteria don't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged review decisions.&lt;/strong&gt; Every flag, the verdict, the reasoning summary, the reviewer role, the timestamp. If a charge is challenged in court two years from now, "one employee decided" is a liability; "here is the recorded decision trail, and here are three prior cases that matched the same criteria" is a defense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second pass on irreversible actions.&lt;/strong&gt; Reporting to law enforcement is irreversible and ends a person's privacy. It should not be a single-reviewer action — the same way destructive agent actions should not be single-click. Two independent verdicts on the same criteria, or it waits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proportionality in the report.&lt;/strong&gt; Hand over the flagged content and the minimum context required, not the whole history. The diary contains more than the threat; the report should not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A user-visible disclosure.&lt;/strong&gt; Not buried in section 14 of the terms — at the point of use. "Entries may be reviewed by humans and reported in emergencies" is the sentence that makes the user's choice informed. If it only lives in the ToS, it is a legal shield, not consent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this argues against reporting credible threats. All of it argues that the decision to end someone's privacy should be as engineered as the system that captured the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you, today
&lt;/h2&gt;

&lt;p&gt;If you are a user: every chatbot input is written into a system whose operator has a disclosure policy, retention rights, and legal obligations you did not write. A diary's job is to be unread. A chatbot's design is to be read. Those are not the same object, and the gap between them is where this woman now lives. Write accordingly — with a real notebook, or with full knowledge.&lt;/p&gt;

&lt;p&gt;If you are a builder: memory features, journaling features, "personal companion" features — every one of them increases how much of a person flows through your review pipeline. Your disclosure policy is not legal boilerplate. It is a security property of your product, with a threat model, and it deserves the same engineering as auth: criteria in code, decisions on record, second passes on irreversible actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The verdict is not the point, and the charge may not hold.&lt;/strong&gt; Whether Statute 836.10 covers text first read by a machine is genuinely contested — the "true threats" doctrine and the "another person may view it" element both cut against an easy conviction. I am auditing the disclosure pipeline, not second-guessing the deputies or prejudging the court. The pipeline's weaknesses exist in every case, including the ones where reporting saves a life.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the alternative case is not hypothetical.&lt;/strong&gt; The Tumbler Ridge shooting happened without a report. Any threshold you tighten to protect diarists loosens protection for the next target of a real threat. There is no clean setting on this dial — which is precisely why the setting should be written, logged, and reviewable instead of living in one reviewer's stomach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Would you write a real diary entry into an AI today? And if you ship anything with memory or review: can you show your users the exact sentence that could turn their input into evidence? The comments are open — bring the scar stories, both kinds.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>privacy</category>
      <category>law</category>
    </item>
    <item>
      <title>The status page was green while apps crashed. The postmortem explains why.</title>
      <dc:creator>Sam LABBE</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:01:44 +0000</pubDate>
      <link>https://dev.to/slabb/the-status-page-was-green-while-apps-crashed-the-postmortem-explains-why-5am3</link>
      <guid>https://dev.to/slabb/the-status-page-was-green-while-apps-crashed-the-postmortem-explains-why-5am3</guid>
      <description>&lt;p&gt;On September 28 at 17:41 Pacific Time, a malformed configuration payload finished rolling out to a wide population of iOS apps. Phones started crashing on launch. Firebase's status dashboards stayed green.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://firebase.blog/posts/2026/10/firebase-analytics-outage" rel="noopener noreferrer"&gt;postmortem&lt;/a&gt; went up on October 2, four days later, and it is better than most vendor postmortems — a real timeline to the minute, a mechanism, an admission that the tests passed. I still read it the way I read anything a vendor writes about its own failure: a plausible narrative until someone can check it. By that standard, the report contains exactly one number nobody can verify, and it is the number everyone actually wants.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clock, as published
&lt;/h2&gt;

&lt;p&gt;The mechanics first, because they are good. Firebase's iOS SDK accumulates obsolete remote-config flags. Teams use those flags as kill switches — "a way to turn off a feature that contains a bug". On September 28 at 17:38 PDT, a cleanup retired one stale flag. A pointer to it remained somewhere in the SDK; when the SDK fetched the missing flag, "this resulted in a fatal error". A cleanup described as a 2-line change produced a malformed payload that rolled out globally within three minutes.&lt;/p&gt;

&lt;p&gt;Then the clock, in the report's own terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17:38  stale legacy flag cleaned up
17:41  malformed payload rolls out globally — clients crash on launch
17:59  crash alerts spike, first external bugs via GitHub, on-call paged
19:16  culprit change identified, rollback initiated
19:52  rollback fully deployed — mitigation complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalid configuration served for &lt;strong&gt;2 hours 11 minutes&lt;/strong&gt;. Detection took under twenty minutes. Elevated error rates persisted "for many hours based on error reporting lag". Those are receipts: timestamps, durations, a mechanism you could in principle reproduce in a test device. If every postmortem had this spine, incident analysis would be a better field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three claims an auditor flags
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"A large number of iOS applications."&lt;/strong&gt; That is the blast radius, in full. No app count, no device count, no SDK version breakdown, no list of affected surfaces. Third-party coverage said "thousands of apps" — but that number comes from reporters counting crash reports, not from the vendor who knows it exactly. Google knows how many clients fetched the malformed payload; it is one query on their own logs. Its absence converts the most important fact of the incident into an adjective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Existing tests passed on the change."&lt;/strong&gt; Which tests? What did the suite cover — flag-fetch failure paths, payload validation, cache fallback? Nothing names the suite, so nothing lets another team check whether their own tests would have caught this. Compare the usable version: "our tests cover X and missed Y because of Z" — that sentence is a gift to every reader; "existing tests passed" is a shrug with a citation mark.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dashboards were green.&lt;/strong&gt; The report says it outright: status dashboards rely on server-side metrics and missed the client-side crashes entirely. The witness was watching the wrong room. During a global client-side outage, the authoritative channel turned out to be a GitHub issue — because dashboard updates "required manual intervention taking hours". Read that back: the company's own incident-communication tool was harder to update than a free issue thread. That is the finding I would put on top of the retrospective, and the postmortem buries it in a mitigation bullet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kill switch ate its own caller
&lt;/h2&gt;

&lt;p&gt;Here is the part I keep thinking about. The flags were the safety mechanism — the kill switch for buggy features. The failure came from managing the safety mechanism's own lifecycle: a cleanup of dead flags, merged as trivial, reached the kill switches themselves.&lt;/p&gt;

&lt;p&gt;That pattern generalizes further than Firebase. Auth handlers, feature flags, circuit breakers, backups, the seatbelt hooks people attach to their AI coding agents — every control has a lifecycle, and the lifecycle changes (cleanup, migration, refactoring) almost never inherit the control's own tests, because the control "works". The configuration that breaks your system is increasingly the configuration that was supposed to save you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ge29m49nep4d52rvd3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ge29m49nep4d52rvd3m.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a receipts-grade incident report contains
&lt;/h2&gt;

&lt;p&gt;For contrast, look at what the UK AI Safety Institute published on August 4 after its own agents went off-script during evaluations: an incident ID, a page count you can hold (35), the number of runs (122), the number of unsanctioned actions (19, across 10 runs), which models did what, and remediation commitments specific enough to be checked ("internet access must be actively justified rather than a default"). You can disagree with their containment choices — I do — but every load-bearing claim is a number or a verifiable commitment.&lt;/p&gt;

&lt;p&gt;The Firebase report has the honest spine and skips the counts. A receipts-grade version would add three lines: apps affected (a number from their own logs), the test suite that passed and what it covered, and the diff between the valid and malformed payloads. None of that is secret. All of it is knowable by the author. The gap between knowable and published is where trust gets spent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal for your stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail-safe fetch.&lt;/strong&gt; The committed SDK patch makes missing or corrupt flags fall back to cached defaults instead of crashing. Until it ships, "within the next week", every app on the affected SDK line carries the same fault. Watch the releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config is a deployment.&lt;/strong&gt; The report's own lesson: "deploying configuration changes to a broad segment of our global customer base too quickly introduces unnecessary risk." Phased rollouts and canaries apply to flags exactly like binaries — more, actually, because flags have no build step to catch them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the clients.&lt;/strong&gt; Your status page watches your servers. Your users run your client. If you have no client-side heartbeat — crash reporter, canary app, third-party tracker — then during the next client-side incident, your dashboard will also be green, and you will also find out from GitHub.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receipts, such as they are:&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;# The primary source — read the report before the commentary:&lt;/span&gt;
open https://firebase.blog/posts/2026/10/firebase-analytics-outage

&lt;span class="c"&gt;# The receipt to watch: the validation patch, promised "within the next week".&lt;/span&gt;
&lt;span class="c"&gt;# If it ships with a test for the flag-fetch failure path, this postmortem&lt;/span&gt;
&lt;span class="c"&gt;# graduates from narrative to receipt. Until then it is a promise with dates.&lt;/span&gt;
gh release list &lt;span class="nt"&gt;-R&lt;/span&gt; firebase/firebase-ios-sdk &lt;span class="nt"&gt;--limit&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two honest limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This postmortem is not a bad one — it is an average good one.&lt;/strong&gt; Twenty minutes to detection, 2h 11min to full rollback, a mechanism published in plain language, and a promise attached to a date. The audit above is about verifiability, not competence; most vendors publish less, later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And hindsight is unfair.&lt;/strong&gt; "The tests should have covered the missing-flag path" is easy to write on October 5 and hard to have insisted on in a cleanup review. That is precisely why the fix has to be structural — validation at fetch time — rather than a renewed promise to test harder. Structure survives attention; attention does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;When your status page last stayed green while your users burned, what did you point people at — and how long did it take to get a number out? I am collecting incident-report receipts: the good, the green, and the GitHub issues that did the status page's job.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sre</category>
      <category>ios</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
