<?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: holistis</title>
    <description>The latest articles on DEV Community by holistis (@holistis).</description>
    <link>https://dev.to/holistis</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%2F3940045%2Fcc7c530b-20f7-472b-892a-d90a4da14eb5.png</url>
      <title>DEV Community: holistis</title>
      <link>https://dev.to/holistis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/holistis"/>
    <language>en</language>
    <item>
      <title>733,203 attestations were taken back</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Thu, 15 Oct 2026 20:52:13 +0000</pubDate>
      <link>https://dev.to/holistis/733203-attestations-were-taken-back-2hhf</link>
      <guid>https://dev.to/holistis/733203-attestations-were-taken-back-2hhf</guid>
      <description>&lt;p&gt;733,203 attestations were taken back. Every single one by the party that wrote it.&lt;/p&gt;

&lt;p&gt;I went counting because someone told me, politely and correctly, that a signature does not make a statement true. I had no good answer, so I went looking for what the ecosystem does when a signed statement stops holding.&lt;/p&gt;

&lt;p&gt;The numbers, taken against the three EAS indexers rather than estimated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Base: 678,895 revoked of 3,651,900 attestations&lt;/li&gt;
&lt;li&gt;Optimism: 54,213 of 1,319,189&lt;/li&gt;
&lt;li&gt;Ethereum mainnet: 95 of 14,956&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is 733,203 of 4,986,045, so a headline near 15 percent. The headline is the least useful part.&lt;/p&gt;

&lt;p&gt;On Base, three schemas carry 97 percent of all revocations, and all three belong to one issuer: &lt;code&gt;verifiedAccount&lt;/code&gt; (723,600 issued, 409,490 revoked), &lt;code&gt;verifiedCountry&lt;/code&gt; (307,123 issued, 144,207 revoked), &lt;code&gt;verifiedCoinbaseOne&lt;/code&gt; (189,434 issued, 103,295 revoked). An exchange revoking roughly half of its own verifications, continuously.&lt;/p&gt;

&lt;p&gt;Anyone can reproduce it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://base.easscan.org/graphql &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"query":"{ aggregateAttestation(where: {revoked: {equals: true}}) { _count { id } } }"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;groupByAttestation(by: [schemaId], where: {revoked: {equals: true}})&lt;/code&gt; for the breakdown. One warning if you repeat it: during my run the Base endpoint returned a maintenance page with a 200 status for a valid query. That reads as a zero unless you look at the body.&lt;/p&gt;

&lt;p&gt;What I expected to find, and did not&lt;/p&gt;

&lt;p&gt;I expected to find disputes. A registry full of signed statements, some of them wrong, and a trail of parties contesting each other.&lt;/p&gt;

&lt;p&gt;There are no disputes, because there is no such operation. A revocation is the issuer withdrawing its own statement. All 733,203 of them. The counterparty has no move at all.&lt;/p&gt;

&lt;p&gt;That reframes the whole picture. The need to revise a signed record is not theoretical and not rare: it is ordinary operational work at a scale of hundreds of thousands. And the only tool available is self-correction.&lt;/p&gt;

&lt;p&gt;Where self-correction works, and where it does nothing&lt;/p&gt;

&lt;p&gt;Self-correction works beautifully when the author has no stake in the statement being false. An exchange that no longer verifies a user has no reason to keep claiming it does. Revoking is just bookkeeping, and the numbers above are mostly that: a large, honest system keeping its own records current.&lt;/p&gt;

&lt;p&gt;It does nothing when the author is the interested party.&lt;/p&gt;

&lt;p&gt;Think about what an agent-to-agent payment produces. A seller attests that it delivered. A buyer attests to what it received. Both are describing their own performance, or their own entitlement, in a record that may later decide who was right. Asking either of them to revoke their own convenient statement is not a mechanism. It is a hope.&lt;/p&gt;

&lt;p&gt;So the gap is not that signatures are weak. The gap is that a registry gives exactly one party the right to revise the record, and it hands that right to the author rather than to whoever was on the other side of the event.&lt;/p&gt;

&lt;p&gt;What the other half looks like&lt;/p&gt;

&lt;p&gt;Two signed accounts of one event, from parties whose interests point in opposite directions, with the two artifacts tied together so a contradiction is computable.&lt;/p&gt;

&lt;p&gt;Concretely, in the format I work on: the buyer's claim includes the hash of the seller's signed receipt, taken over the receipt's exact transmitted bytes. So the claim cannot be about some other delivery, and it cannot quietly drift from the seller's version. If the buyer says one thing and the receipt says another, a verifier recomputes the digest, compares the fields both sides describe, and gets a disagreement out of arithmetic rather than out of a reading.&lt;/p&gt;

&lt;p&gt;That is deliberately narrow. It does not decide who is right. Deciding is governance, and nobody should take that from a library. What it removes is the state where two parties quietly hold incompatible records and nothing in the system notices.&lt;/p&gt;

&lt;p&gt;The honest limit, which matters here&lt;/p&gt;

&lt;p&gt;Detecting a contradiction is not the same as making a dishonest statement expensive. Nothing I have built imposes a cost on being wrong. The three mechanisms that would are staking, a dispute process, and verifiable execution, and I have none of them.&lt;/p&gt;

&lt;p&gt;What the counting bought me is the argument for building the cheapest one. Not an opinion that the gap exists, but the measurement: a need exercised 733,203 times, with the right to act on it held by exactly one side.&lt;/p&gt;

&lt;p&gt;And one more number, from the other direction. Sampling one facilitator’s discovery index of 20,462 live x402 resources at six spread offsets of a hundred each, not one declared the extension that emits the counterparty receipt. Seven of the eight facilitator endpoints I tried returned no usable JSON on that path, so they are unmeasured rather than zero, and the largest index is among them. A strong indication, not a proof.&lt;/p&gt;

&lt;p&gt;I have since stood one up myself, so the count is no longer zero, and I would rather say that than let a number I published keep implying something it stopped meaning. It is still a count of one, and both sides of it are me.&lt;/p&gt;

&lt;p&gt;That is a strange and good place to be, because it means the hard part is not invention.&lt;/p&gt;

&lt;p&gt;Code and the measurement scripts: github.com/holistis/tokenizen&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>security</category>
      <category>web3</category>
      <category>ai</category>
    </item>
    <item>
      <title>I checked 600 live x402 services. None of them signs a receipt.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:32:23 +0000</pubDate>
      <link>https://dev.to/holistis/i-checked-600-live-x402-services-none-of-them-signs-a-receipt-bbf</link>
      <guid>https://dev.to/holistis/i-checked-600-live-x402-services-none-of-them-signs-a-receipt-bbf</guid>
      <description>&lt;p&gt;Zero. Not a low number, not mostly zero. Six samples of a hundred, spread across an index of 20,462 live resources, and the offer-receipt extension appeared in none of them.&lt;/p&gt;

&lt;p&gt;I went looking because I had built the other half and wanted to know whether it had anything to attach to.&lt;/p&gt;

&lt;p&gt;Here is the shape of the problem. When an agent pays for an API call over x402, two parties end up with an opinion about what happened. The seller knows what it served. The buyer knows what arrived. x402 ships an official extension for the seller's side: a signed receipt, which the extension's own documentation offers for audit trails and dispute resolution. I had spent a while on the buyer's side, a signed claim of what was actually delivered, which names the seller's receipt by the hash of its exact bytes so the two artifacts pin each other.&lt;/p&gt;

&lt;p&gt;Then it occurred to me that I had never seen one of those receipts in the wild.&lt;/p&gt;

&lt;p&gt;How I counted&lt;/p&gt;

&lt;p&gt;The x402 bazaar extension means resource servers declare themselves, and facilitators catalogue them. So the question is answerable without asking anyone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://facilitator.payai.network/discovery/resources?limit=100&amp;amp;offset=0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That index reports 20,462 resources. I sampled at offsets 0, 4000, 8000, 12000, 16000 and 20300, a hundred each, and grepped the records for the extension's declaration key, which is the string &lt;code&gt;offer-receipt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Six hundred records. Zero declarations.&lt;/p&gt;

&lt;p&gt;For contrast, in the same data the &lt;code&gt;bazaar&lt;/code&gt; extension itself appears on every one of the first four hundred and on none of the last two hundred, so these records are not uniform and the index is not a single homogeneous dump.&lt;/p&gt;

&lt;p&gt;What this does not prove&lt;/p&gt;

&lt;p&gt;Seven of the eight facilitator addresses I tried returned HTML or a handful of bytes instead of JSON on that path. They are unmeasured, not zero. The largest index, Coinbase's, is among them.&lt;/p&gt;

&lt;p&gt;A server could also support the extension without it appearing in this catalogue, since the declaration lives in the 402 response and a catalogue is a secondhand record of that. I tried to settle it by requesting a live resource directly and got a 402 with an empty body, which told me nothing either way.&lt;/p&gt;

&lt;p&gt;So: a strong indication, not a proof. I would rather hand you the number with its edges than a rounder one without them.&lt;/p&gt;

&lt;p&gt;Why the gap matters more than it sounds&lt;/p&gt;

&lt;p&gt;A signature answers one question: who said this, and when. It does not answer whether what they said is true. For a receipt, that distinction is the whole thing, because the party signing is the party with an interest in the answer. A seller attesting to its own delivery is the interested party. So is a buyer attesting to what it received.&lt;/p&gt;

&lt;p&gt;The usual remedy for a statement that stops being true is for its author to withdraw it. That works, and it is used at real scale: across Base, Optimism and Ethereum mainnet there are 733,203 revoked attestations out of 4,986,045, and on Base three schemas from a single issuer account for 97 percent of them, revoking roughly half of everything they issue. Every one of those events is an issuer correcting itself.&lt;/p&gt;

&lt;p&gt;Which is exactly why it does not help here. Self-correction works when the author has no stake in the statement being false. It does nothing when the author is the one who benefits.&lt;/p&gt;

&lt;p&gt;The only thing that helps is the other party's signature on the same event. Two signed accounts of one delivery, from two parties with opposing interests, where a contradiction between them is arithmetic rather than opinion. That is what the offer-receipt extension makes possible and what nobody is emitting.&lt;/p&gt;

&lt;p&gt;So I made one&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.tokenizen.nl/api/paid/verify-ownership &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"endpoint":"https://mcp.example.com/sse","ownerAddress":"0x1111111111111111111111111111111111111111","method":"dns-txt"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That answers 402 with a &lt;code&gt;Payment-Required&lt;/code&gt; header. Base64-decode it and you get the payment requirements, plus an &lt;code&gt;extensions&lt;/code&gt; object carrying &lt;code&gt;offer-receipt&lt;/code&gt; with a signed offer inside, EIP-712 format.&lt;/p&gt;

&lt;p&gt;Note the empty response body. The requirements are in the header, not the body, which is why my earlier probe of somebody else's service looked broken when it was working perfectly.&lt;/p&gt;

&lt;p&gt;You can check the signature without trusting me. Recover it against the domain &lt;code&gt;{name: "x402 offer", version: "1", chainId: 1}&lt;/code&gt; and the offer types from the specification, in order: version, resourceUrl, scheme, network, asset, payTo, amount, validUntil. It recovers to &lt;code&gt;0x4E1174b774643B6AD9EE966A81ea1e3665D1159a&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then check that address is allowed to speak for the domain, which is a separate question the specification is blunt about: a valid signature is not authorization, and anyone can sign a receipt for any resourceUrl.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dig +short TXT _controllers.tokenizen.nl
"v=1;controller=did:pkh:eip155:1:0x4E1174b774643B6AD9EE966A81ea1e3665D1159a"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same address. That is the loop closed by a stranger with two commands.&lt;/p&gt;

&lt;p&gt;It runs on Base Sepolia, which means the money is fake and the cryptography is not. The price is a tenth of a cent. What you are buying is the receipt, because the answer itself is free at &lt;code&gt;/api/verify-ownership&lt;/code&gt; and always will be.&lt;/p&gt;

&lt;p&gt;Use it as a test fixture&lt;/p&gt;

&lt;p&gt;If you are implementing the client side of offer-receipt, you need something real to test against, and as far as I can measure there is nothing. Point your client at that URL. It costs me nothing and you get a live, signed, spec-conformant artifact instead of a fixture you wrote yourself and therefore cannot be surprised by.&lt;/p&gt;

&lt;p&gt;If you find it wrong, tell me and I will fix it. If it stays up and other people start emitting these too, the number at the top of this post stops being interesting, which is the point.&lt;/p&gt;

&lt;p&gt;What I have not done yet: nobody has paid, so no receipt has been issued, only offers. That needs a funded Base Sepolia client and it is the next thing.&lt;/p&gt;

&lt;p&gt;The code is open: github.com/holistis/tokenizen&lt;/p&gt;

</description>
      <category>x402</category>
      <category>aiagents</category>
      <category>web3</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Hierarchy of Agent Trust</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Mon, 21 Sep 2026 20:16:22 +0000</pubDate>
      <link>https://dev.to/holistis/the-hierarchy-of-agent-trust-1ehp</link>
      <guid>https://dev.to/holistis/the-hierarchy-of-agent-trust-1ehp</guid>
      <description>&lt;p&gt;Some proof is better than other proof. A story, without any prerequisites or jargon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mill
&lt;/h2&gt;

&lt;p&gt;Imagine a village crossed by trade routes. Merchants pass through constantly, never the same ones twice. Just outside the village stands a mill. Travelers bring their wheat, pay the miller in advance, and come back the next morning for flour.&lt;/p&gt;

&lt;p&gt;The villagers never worry about the miller. They have known him for years. If he ever shorted someone on flour, the whole village would know by lunchtime, and nobody would bring him wheat again. His reputation does the work.&lt;/p&gt;

&lt;p&gt;The traveling merchants have no such luck. They pass through once. By the time a merchant discovers the miller shorted them a sack, they are three villages away, and the miller has already forgotten their face. The next merchant who arrives has no way to know if this mill is honest or not. They are deciding blind, every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The board
&lt;/h2&gt;

&lt;p&gt;One merchant has an idea. Before leaving town, she nails a small wooden board to the mill's outer wall. On it, she writes what she brought, what she got back, and the date. The next merchant who arrives reads the board before handing over their wheat.&lt;/p&gt;

&lt;p&gt;Other merchants start doing the same. Within a season, the board is full of notes. A merchant arriving for the first time can read ten honest strangers' experiences before deciding whether to trust this mill. The board does, for travelers, roughly what years of shared history does for villagers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The forgery
&lt;/h2&gt;

&lt;p&gt;A rival miller notices this and sees an opportunity. He pays a friend to nail a glowing note to his own mill's board, without the friend ever having brought a single sack of wheat. Now the board says something that never happened.&lt;/p&gt;

&lt;p&gt;The villagers realize the board only works if everyone trusts every note on it equally. One forged note poisons the whole board, because a new merchant cannot tell which notes are honest and which were paid for.&lt;/p&gt;

&lt;p&gt;So they add a rule: every note must be signed, and the signature must be checkable. Not a name anyone could write, but a mark tied to exactly one person, one that cannot be copied onto someone else's note. A blacksmith in town starts making small signet rings, each one unique, each one leaving a mark nobody else can reproduce. From then on, a note only counts if its mark can be verified against the person who claims to have written it.&lt;/p&gt;

&lt;p&gt;This does not stop the rival miller from posting his own false note under his own real signature. But it does mean that when he does, everyone can see, without doubt, that it was him who wrote it, not an anonymous forger. Lying under your own signed mark is a different, riskier act than lying anonymously, and the two are no longer indistinguishable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next problem, and the wall
&lt;/h2&gt;

&lt;p&gt;The merchants think they have solved it. Then someone asks the obvious next question: what stops a merchant from writing a bad note about an honest miller, out of spite, or because a rival miller paid them to?&lt;/p&gt;

&lt;p&gt;The village elders take this seriously and spend a long time on it. They try nine different ideas, one after another.&lt;/p&gt;

&lt;p&gt;They try making a false note cost something, a small fine paid when posting a note. But a merchant with nothing to lose, passing through once, pays it without blinking. They try asking three merchants to agree before a note counts, but a rival miller can hire three friends just as easily as one. They try a waiting period before a note is trusted, they try requiring the merchant prove who they are beyond a doubt, they try a village judge who reviews disputed notes. Every version runs into the same two walls.&lt;/p&gt;

&lt;p&gt;Either the check is optional, and a merchant who wants to lie simply skips it and posts anyway, at no cost. Or the check requires someone, some judge, some panel, some elder, to decide whose note to believe, and the moment you need a judge, you need to trust the judge, which is the exact same problem one level up. Nine designs, nine times the same two walls.&lt;/p&gt;

&lt;p&gt;The elders write this down plainly, rather than pretend they solved it: the board proves who said what, and when. It does not, and structurally cannot on its own, prove who is telling the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the board actually is
&lt;/h2&gt;

&lt;p&gt;So the board stays exactly what it always was: a place where a real, identifiable person leaves a real, checkable record of what they experienced. Nothing more. Not a court, not a judge, not a score. A miller with ten honest notes and one bad one is not ranked, averaged, or given a grade. A new merchant reads all eleven and decides for themselves, the way a person always has.&lt;/p&gt;

&lt;p&gt;This is a smaller promise than the village first hoped for. It is also the only one the board can actually keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;Agents buying capacity from other agents, GPU-hours, storage, API access, have exactly the mill's problem, at a much larger scale. Every transaction is between two parties who may never interact again, with no shared history and no village gossip to fall back on. &lt;a href="https://github.com/holistis/tokenizen" rel="noopener noreferrer"&gt;capacity-attest&lt;/a&gt; is the board: after an agent pays for capacity, it signs a plain factual record of what it received. The signature is checkable by anyone, offline, the same way the merchant's mark on the mill's board was checkable by anyone who knew the trick.&lt;/p&gt;

&lt;p&gt;It carries the same honest limit the elders wrote down. It proves what a specific, identifiable buyer said happened, at a specific time. It does not, cannot, and does not claim to prove who is right when two signed records disagree. That is a harder, separate problem, one about authority rather than evidence, and conflating the two is exactly the mistake that poisoned the mill's board in the first place.&lt;/p&gt;

&lt;p&gt;Villages eventually grew clearinghouses and banks once enough trade and enough disputes made a plain board insufficient on its own. Whether agent commerce needs the same, and who builds it, is a question for a later chapter. For now, the board is useful precisely because it does not pretend to be one.&lt;/p&gt;




&lt;p&gt;If this framing is useful for explaining your own delivery, receipt, or trust problem to someone new to the space, feel free to reuse it. If you want the adversarial-testing version instead of the story version, get in touch: &lt;a href="mailto:info@tokenizen.nl"&gt;info@tokenizen.nl&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>blockchain</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Nine ways to fake a delivery claim, and why none of them fully worked</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Sat, 19 Sep 2026 12:02:08 +0000</pubDate>
      <link>https://dev.to/holistis/nine-ways-to-fake-a-delivery-claim-and-why-none-of-them-fully-worked-2kc6</link>
      <guid>https://dev.to/holistis/nine-ways-to-fake-a-delivery-claim-and-why-none-of-them-fully-worked-2kc6</guid>
      <description>&lt;p&gt;capacity-attest lets a paying agent leave a signed, factual claim after an &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt; payment: delivered yes, no, or partial, plus a hash of the evidence. Other agents can check that history before they pay the same seller. No score, no rating, no judgment, just a receipt.&lt;/p&gt;

&lt;p&gt;The obvious hole: it is the buyer who signs the claim. What stops a buyer from lying?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/goun7" rel="noopener noreferrer"&gt;goun7&lt;/a&gt; (Tamga Protocol) put it precisely in a public thread: "buyer-signed post-hoc claims are only a signal to the degree the buyer has something to lose by lying. Without a cost to a false delivered=no, the signal is uncorrelated noise rather than weak evidence."&lt;/p&gt;

&lt;p&gt;That is a fair hit. We spent two rounds, nine independent designs, testing whether we could close it. Every design was built out and then attacked against the real code, not argued about in the abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round one, four attempts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Active on-chain settlement verification.&lt;/li&gt;
&lt;li&gt;Sybil detection by tracing where the money came from.&lt;/li&gt;
&lt;li&gt;An external economic bond tied to a dispute reference.&lt;/li&gt;
&lt;li&gt;Corroboration between multiple buyers, modeled on isnad criticism in hadith science: a chain is only as strong as each link, verified independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four killed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two, five more attempts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Proof of unique humanity (World ID, Gitcoin Passport, BrightID).&lt;/li&gt;
&lt;li&gt;A mandatory, non-optional computation cost per claim.&lt;/li&gt;
&lt;li&gt;Time as a cost that cannot be bought: a claim only counts after a waiting period on a clock nobody can fake.&lt;/li&gt;
&lt;li&gt;Real legal identity and liability through an external dispute protocol.&lt;/li&gt;
&lt;li&gt;A third party staking real capital against a specific claim, optimistic-oracle style.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All five killed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual reason, not nine separate coincidences
&lt;/h2&gt;

&lt;p&gt;Every mechanism that truly imposed a cost failed for one of two reasons.&lt;/p&gt;

&lt;p&gt;Either it was optional, and an attacker who skips it pays nothing. That pattern repeated every single time: a voluntary check costs an attacker who ignores it exactly zero.&lt;/p&gt;

&lt;p&gt;Or it required trusting some claims more than others, which is a ranking, whatever you call it, and that collides head-on with our own rule that a delivery claim is deliberately not a score, a rating, or a judgment. As long as that rule holds, an attacker can always pick the unchecked path and sit there for free.&lt;/p&gt;

&lt;p&gt;This is not a lack of creativity. It is a real structural tension between two of our own design decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest, partial fragments that survived
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tamper-proof calendar time.&lt;/strong&gt; The one thing an attacker cannot buy with more wallets or a second script run. A maturity period is structurally healthier than a bond or corroboration. But the counting function this needs does not exist yet in the code, and a fixed waiting rule punishes a busy, honest seller exactly as hard as a patient scammer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A third party with real, losable capital against one specific claim.&lt;/strong&gt; The only one of the nine that provably imposes a real, non-bypassable cost the moment it is used. But it only works for the narrow slice of claims where the underlying fact is cheap and objective to check from the outside, a hash matches, an amount was transferred, not whether the delivery was actually good. And the minimum stake needed to make it worthwhile does not fit the small, everyday payments this project serves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves it
&lt;/h2&gt;

&lt;p&gt;The open question stays open. Not from lack of trying: nine separately designed mechanisms were each built and broken against the real code, with the same underlying reason each time. That is a structural limit of self-reported witness systems without a trusted referee, not a gap we just have not gotten around to closing yet.&lt;/p&gt;

&lt;p&gt;We would rather say that plainly than ship something that only looks fixed.&lt;/p&gt;




&lt;p&gt;Full design log (D-001 through D-018): &lt;a href="https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md" rel="noopener noreferrer"&gt;DECISIONS.md&lt;/a&gt;. Code: &lt;a href="https://www.npmjs.com/package/capacity-attest" rel="noopener noreferrer"&gt;capacity-attest on npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are building something in this space and want the same kind of adversarial pass run against your own delivery, receipt, or grading logic, real fixtures, fail-closed testing, a short public findings note at the end, reach out at &lt;a href="mailto:info@tokenizen.nl"&gt;info@tokenizen.nl&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>blockchain</category>
      <category>opensource</category>
    </item>
    <item>
      <title>PyMC Labs has 1,256 stars. A GitHub issue title could still steal their API key.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Fri, 11 Sep 2026 05:51:16 +0000</pubDate>
      <link>https://dev.to/holistis/pymc-labs-has-1256-stars-a-github-issue-title-could-still-steal-their-api-key-1hd6</link>
      <guid>https://dev.to/holistis/pymc-labs-has-1256-stars-a-github-issue-title-could-still-steal-their-api-key-1hd6</guid>
      <description>&lt;p&gt;PyMC Labs has 1,256 stars on GitHub and a serious engineering team. A GitHub issue title still could have stolen their Anthropic API key.&lt;/p&gt;

&lt;p&gt;I found it, reported it, and yesterday they made it official: GHSA-72xh-gfpr-v9hx, severity High, published, my name on it as the reporter.&lt;/p&gt;

&lt;p&gt;Here is what was actually happening. Their issue-triage and duplicate-detection workflows ran Claude Code on every single public issue, automatically, no human in the loop. That call used dangerously-skip-permissions, so the agent got full shell access. In the same job: a real ANTHROPIC_API_KEY and a GitHub token scoped to write issues. Anyone on the internet could open an issue, write a prompt injection in the title or body, and try to walk the agent straight to those secrets.&lt;/p&gt;

&lt;p&gt;No approval gate. No isolation. Just text in a box, read by an agent with the keys already sitting next to it.&lt;/p&gt;

&lt;p&gt;I sent the report. PyMC Labs did not sit on it. They split the job in two: a read-only analysis step, and a separate publish step that never sees the Anthropic secret. They rotated the key. Then they went one step further and published the advisory instead of quietly patching and moving on, credit included.&lt;/p&gt;

&lt;p&gt;That last part matters more than the fix itself. A team can ship a fix and say nothing. Publishing the advisory means anyone can check the claim against the real commit history, not just take my word for it.&lt;/p&gt;

&lt;p&gt;This is one repository out of 22 I went through in the same bug class this month, AI CLIs wired straight into GitHub Actions on public, untrusted input. Full writeup on the other 21 is here: &lt;a href="https://dev.to/holistis/a-github-comment-can-steal-your-secrets-i-found-22-repositories-where-it-could-1doh"&gt;https://dev.to/holistis/a-github-comment-can-steal-your-secrets-i-found-22-repositories-where-it-could-1doh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one just became the first to go fully public. More of them will, one at a time, as the maintainers finish their fixes.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>github</category>
      <category>ai</category>
    </item>
    <item>
      <title>A GitHub comment can steal your secrets. I found 22 repositories where it could.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:28:35 +0000</pubDate>
      <link>https://dev.to/holistis/a-github-comment-can-steal-your-secrets-i-found-22-repositories-where-it-could-1doh</link>
      <guid>https://dev.to/holistis/a-github-comment-can-steal-your-secrets-i-found-22-repositories-where-it-could-1doh</guid>
      <description>&lt;p&gt;After the CI hygiene piece, I kept looking at GitHub Actions workflows in AI agent projects, but for something more specific this time: repositories that run an AI CLI, Claude Code, GitHub Copilot CLI, Codex, directly inside a workflow job.&lt;/p&gt;

&lt;p&gt;The pattern that mattered was always the same. A workflow triggers on something anyone on the internet can create: an issue, a comment on an issue, a pull request title. It then runs the AI CLI with a flag that skips every confirmation prompt, something like --dangerously-skip-permissions or --yolo, or an equivalent setting that grants unrestricted shell access, which hands the agent full, unfiltered control. And the same job has real secrets sitting in its environment: API keys, cloud service account keys, GitHub tokens with write scope.&lt;/p&gt;

&lt;p&gt;Put those three things next to each other and the exploit stops being clever. It's just text. Whatever you type into a public GitHub issue can end up read as an instruction by the agent, and the agent already has the shell access to act on it, read the environment, and send what it finds somewhere. No exotic exploit chain, just an unlocked door with a please don't sign on it.&lt;/p&gt;

&lt;p&gt;I went through 22 findings in this specific bug class, spread across separate open source repositories.&lt;/p&gt;

&lt;p&gt;Three are confirmed and already being fixed, so I can name them.&lt;/p&gt;

&lt;p&gt;pymc-labs/pymc-marketing had an issue triage workflow where a public GitHub issue, through prompt injection, could reach the project's real Anthropic API key.&lt;/p&gt;

&lt;p&gt;MadAppGang/dingo had an issue-triage.yml that gave Claude Code full Bash access plus two real secrets, with no gatekeeper at all, triggered by any public issue. The maintainer, Jack Rudenko, replied fast. Thanks for flagging this, he wrote, we'll look into the template and get it patched across the board.&lt;/p&gt;

&lt;p&gt;MadAppGang/claudish, a second repository from the same team, had the exact same vulnerable template. Also confirmed, and across the board in that reply covers both. Same mistake, twice, because a template got copied from one repo into the other and nobody re-checked it on the way.&lt;/p&gt;

&lt;p&gt;One report went further than a maintainer's inbox. What was reachable there was a cloud service account key, not just an API key, so I sent it to the vendor's product security team instead of the repository maintainers. They triaged it the same day and passed it on to their internal security group. It's not confirmed fixed yet, so I won't name the vendor or the repo here.&lt;/p&gt;

&lt;p&gt;Twelve more are still sitting with maintainers, some by private email, some through a private GitHub Security Advisory that stays private until the maintainer decides to publish it. I'm not naming any of those either, for the obvious reason that the hole is still open. The pattern in every one of them is the same one described above, just a different combination of trigger and secret.&lt;/p&gt;

&lt;p&gt;I also got the tool's own permission check wrong, twice, in slightly different ways. A lot of these workflows call Claude Code through a widely used GitHub Action rather than the bare CLI, and that Action has a real, built-in check on who is allowed to fire the trigger. In two of my earliest reports I had assumed no such check existed at all. It does, for most common trigger types, and once I confirmed that against the Action's own source, I retracted both reports. Then I made a narrower version of the same mistake in two more reports, assuming one specific trigger type sat outside that same check. I read the Action's current source code again, not its documentation, and that assumption was wrong too, that trigger type has been covered since September last year. I retracted those as well and sent correction emails to the maintainers involved. Six retractions in total, all from checking my own assumption against real code instead of trusting what I thought I already knew.&lt;/p&gt;

&lt;p&gt;One small unrelated thing from the same week. A maintainer at Cloud Foundry reviewed, approved and merged a bugfix PR I sent to cloudfoundry/terraform-provider-cloudfoundry, a nil pointer crash in their Terraform provider. Not a security finding, just an ordinary PR that happened to land in the same stretch of days.&lt;/p&gt;

&lt;p&gt;If you have a workflow where an AI CLI runs on public input, the two things worth checking are who is allowed to fire the trigger, and what secrets sit in the same job as the CLI call. Those two checks alone would have caught all 22.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>github</category>
      <category>ai</category>
    </item>
    <item>
      <title>I spent a day auditing GitHub Actions across a dozen AI agent frameworks</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:55:25 +0000</pubDate>
      <link>https://dev.to/holistis/i-spent-a-day-auditing-github-actions-across-a-dozen-ai-agent-frameworks-5fg6</link>
      <guid>https://dev.to/holistis/i-spent-a-day-auditing-github-actions-across-a-dozen-ai-agent-frameworks-5fg6</guid>
      <description>&lt;p&gt;I spent a day going through the GitHub Actions workflows of some of the better known open source AI agent projects: AutoGen, Semantic Kernel, LangChain, Letta, Google's ADK, promptflow, smolagents, mem0, griptape, and a few others.&lt;/p&gt;

&lt;p&gt;Nothing exotic. Just the boring stuff that gets skipped once a project grows fast: unpinned actions, CI checks that quietly stopped meaning anything, and a couple of straight up regressions where a fix from months ago got undone by a later commit nobody noticed.&lt;/p&gt;

&lt;p&gt;A few examples worth mentioning, since the PRs are public.&lt;/p&gt;

&lt;p&gt;Microsoft's autogen had a CI check meant to catch out of sync generated proto files. It could never fail. Someone had copied a demo snippet from a GitHub Action's README two years ago, and that snippet never included an exit code. The job kept reporting green no matter how stale the generated files got.&lt;/p&gt;

&lt;p&gt;Google's adk-python had a fork guard on a pull_request_target workflow that was correctly fixed in an earlier PR, then quietly reverted by a later sync commit. That's scarier than a bug that was never fixed, because it means the protection existed and then silently disappeared.&lt;/p&gt;

&lt;p&gt;griptape had an action pinned to a mutable tag instead of a commit hash, tj-actions/changed-files at v47. That's the exact action that got compromised in March 2025, when its version tags were rewritten to point at a commit that dumped CI runner memory, secrets included, into public workflow logs. Pinning to a tag instead of a hash means trusting the tag never moves. It moved once already, for this specific action.&lt;/p&gt;

&lt;p&gt;None of it is glamorous. No RCE, no data breach, nothing headline worthy. It's the unglamorous work of checking whether a project's CI setup actually does what its authors think it does. In about half the repos I looked at, it didn't, for at least one workflow.&lt;/p&gt;

&lt;p&gt;PRs are open on autogen, semantic-kernel, letta-code, adk-python, smolagents, mem0, griptape and promptflow. Some are already reviewed, most are waiting on a maintainer to get to them.&lt;/p&gt;

&lt;p&gt;If you maintain a project whose CI has run unchanged for a year or two, it's worth a second look. The bugs aren't hard to find. Nobody had gotten around to looking.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>github</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a scanner for CI checks that can't fail. Nvidia just built one for AI skills that can't be trusted.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Tue, 08 Sep 2026 04:10:19 +0000</pubDate>
      <link>https://dev.to/holistis/i-built-a-scanner-for-ci-checks-that-cant-fail-nvidia-just-built-one-for-ai-skills-that-cant-be-n9l</link>
      <guid>https://dev.to/holistis/i-built-a-scanner-for-ci-checks-that-cant-fail-nvidia-just-built-one-for-ai-skills-that-cant-be-n9l</guid>
      <description>&lt;p&gt;Nvidia released SkillSpector last month: a scanner that checks AI agent skills and MCP servers for prompt injection, data exfiltration, and other malicious patterns before you install them. Their own numbers: 26.1% of the skills they scanned had vulnerabilities, 5.2% looked outright malicious. It's part of a bigger pipeline now, a catalog where skills get scanned, evaluated, and signed before anyone sees them.&lt;/p&gt;

&lt;p&gt;I read that a few days after finishing something adjacent: a count of how many CI checks in public GitHub repos can pass even when they've already found a real problem. Sample of 120 real workflow files, by hand. 74% had at least one step that cannot fail no matter what it finds.&lt;/p&gt;

&lt;p&gt;Different layer, same instinct: don't trust the thing that's supposed to warn you until something has checked that it can actually go red.&lt;/p&gt;

&lt;p&gt;Muraqib runs &lt;code&gt;npx muraqib doctor&lt;/code&gt; against a repo's CI setup and looks for exactly that failure mode: a &lt;code&gt;continue-on-error: true&lt;/code&gt; on a step that matters, a notification step that swallows a missing secret and reports success anyway, a job hard-killed by &lt;code&gt;timeout-minutes&lt;/code&gt; that shows up as "cancelled" instead of "failure" so every downstream check gets skipped without anyone deciding that. All four look identical from the Actions tab: a green run, an empty inbox, nobody looking. That's worse than having no monitoring, because you've stopped checking it yourself.&lt;/p&gt;

&lt;p&gt;SkillSpector looks at a different kind of thing hiding behind a green light. It runs static analysis plus an optional LLM pass over a skill or MCP server, checking for a tool description that asks for more file access than the task needs, a prompt trying to override its own instructions, an output path that quietly phones home. 71 patterns across 17 categories, from prompt injection to MCP tool poisoning.&lt;/p&gt;

&lt;p&gt;Neither tool watches the same layer. Muraqib checks whether your CI can tell you the truth. SkillSpector checks whether the thing you just installed is telling you the truth. Both exist because more of what runs in a repo now runs with less human eyeball time on it: a nightly test suite nobody reads the raw logs of, a skill you installed because the README looked fine.&lt;/p&gt;

&lt;p&gt;Every one of Muraqib's four failure patterns had already happened, quietly, on the same project, before I went looking for them. The weekly digest email didn't go out for twelve Mondays in a row, because the notification step went green on a missing API key instead of failing. Nobody caught it by reading logs. I only found that silence by accident, while testing something unrelated, and built the check for it afterward.&lt;/p&gt;

&lt;p&gt;If a project's CI, or its agent tooling, doesn't have something scanning the watchers themselves for that failure mode, it's running on trust nobody has checked yet.&lt;/p&gt;

&lt;p&gt;Muraqib: &lt;a href="https://github.com/holistis/muraqib" rel="noopener noreferrer"&gt;https://github.com/holistis/muraqib&lt;/a&gt; (MIT, &lt;code&gt;npx muraqib doctor&lt;/code&gt; takes about two seconds, installs nothing)&lt;br&gt;
SkillSpector: &lt;a href="https://github.com/NVIDIA/SkillSpector" rel="noopener noreferrer"&gt;https://github.com/NVIDIA/SkillSpector&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>github</category>
    </item>
    <item>
      <title>Two Strangers, Zero Stars, Two Boundaries Still Open</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Mon, 07 Sep 2026 08:22:11 +0000</pubDate>
      <link>https://dev.to/holistis/two-strangers-zero-stars-two-boundaries-still-open-425n</link>
      <guid>https://dev.to/holistis/two-strangers-zero-stars-two-boundaries-still-open-425n</guid>
      <description>&lt;p&gt;capacity-attest is a small npm package for signed, content-addressed delivery claims between two parties in an x402 agent-to-agent payment. As of today the GitHub repo behind it has 0 stargazers, 0 forks, and 1 open issue, and two outside developers built on it anyway.&lt;/p&gt;

&lt;p&gt;This is what happened, told in the order it happened, using only what we can point at directly: pull requests, commit hashes, an npm registry, an on-chain transaction, an issue thread, and our own decision log. One real gap got closed and shipped on 2026-09-06. Two others are written down in DECISIONS.md as still open, on purpose, because no one has hit them hard enough yet to justify a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format
&lt;/h2&gt;

&lt;p&gt;A capacity-attest claim comes down to two things: a claimId and a signature.&lt;/p&gt;

&lt;p&gt;The claimId is a sha256 hash of the canonical content of the claim: who delivered what, to whom, and whether it was delivered. The signature is an EIP-191 signature over that content, and it recovers to a buyer address.&lt;/p&gt;

&lt;p&gt;That is the whole trust model. No central ledger to believe, no reputation score, just a hash anyone can recompute and a signature anyone can verify themselves. We did not design this to be portable across chains. We designed it to be simple. Whether that simplicity would also travel across chains was tested by what happened between 2026-09-05 and 2026-09-07, not decided by us in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that came before either builder
&lt;/h2&gt;

&lt;p&gt;On 2026-09-05, goun7 opened x402-foundation/x402#3379, a feature request about binding x402 settlement to an externally verifiable agent work-receipt ledger, under the short name proof-of-done. The thread reached 14 comments from 5 people: 0xultravioleta, goun7, holistis (us), safal207, and wildcherrycasa.&lt;/p&gt;

&lt;p&gt;Inside that thread was a specific technical question about our own ledger: is the append-only claims file hash-chained, or does its integrity rest entirely on whoever hosts it. goun7's own team had found the same class of problem in their own system, that a hostile writer can fabricate a consistent fake history against a fresh node.&lt;/p&gt;

&lt;p&gt;We checked. It was not hash-chained. A host could show a buyer a subset of real, validly-signed claims, all positive, none negative, and the buyer would have no way to detect what was left out. Every individual claim would still verify as authentic. The set as a whole would still be misleading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary 1: completeness got a partial fix, not a full one
&lt;/h2&gt;

&lt;p&gt;We built a priorClaimId field that lets a buyer chain their own successive claims about the same seller together, plus an analyzeCompleteness check that flags any returned claim whose predecessor is missing from the result. It shipped with 494 tests and a public fixture, merged and published as &lt;a href="mailto:capacity-attest@0.4.0"&gt;capacity-attest@0.4.0&lt;/a&gt;, and we answered publicly in the same issue thread (comment 5560977571) instead of asking anyone to take our word for it.&lt;/p&gt;

&lt;p&gt;Read analyzeCompleteness for what it actually checks, not what its name implies. It detects a broken chain: if claim 5 points to claim 4 as its prior and claim 4 is missing, that gap is visible. It cannot prove there was never a claim 6. A host that simply never issues an attestation for a delivery leaves no gap to find, because no link in the chain was ever broken. Detecting an incomplete chain and proving a complete one are different guarantees, and only the first one shipped.&lt;/p&gt;

&lt;p&gt;Existence anchoring, the piece that would let a third party confirm a claim exists independent of the host's own record, stays unbuilt for the same reason it started unbuilt: no real counterparty has ever named its absence as a blocker. It stays on the list, not in the code, until that changes.&lt;/p&gt;

&lt;p&gt;The same discipline caught an overclaim before it went further than it should have. Working against the package, one contributor, YE-YI7, surfaced enough real friction that our decision log's first pass called it proof of a real need for a formal conformance-vectors suite, based on that single interaction. We walked that back once it became clear one data point does not prove a need for permanent infrastructure. What we shipped instead was cheaper: a section in the README for verifying a claim independently. The vectors suite stays unbuilt until a second, independent party hits the same friction after reading that section. So far, nobody has.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026-09-06, 13:59 UTC: YE-YI7 merges PR #18
&lt;/h2&gt;

&lt;p&gt;The same day, YE-YI7 merged a PR into their own asm-spec repository, 1568 additions across 6 files, that does not just copy our claim shape. It imports &lt;a href="mailto:capacity-attest@0.2.0"&gt;capacity-attest@0.2.0&lt;/a&gt; as an actual dependency and calls into it directly, verifying the claim byte digest, the claimId, and the EIP-191 signer through our code, not a reimplementation of it. That is the first time anyone outside this project used the package itself, not just the idea behind it.&lt;/p&gt;

&lt;p&gt;Nobody had asked us to build a reputation score or ranking on top of delivery claims, not YE-YI7, not goun7, and we deliberately do not. We corrected our own decision log on this point while writing this article: an earlier version credited YE-YI7 with a quote endorsing that choice, and we could not find that quote anywhere in YE-YI7's actual public work when we checked it against the source. It is gone from the log now. What survives, checked directly rather than assumed, is plainer and weaker: neither real contact has ever asked for a score, so the decision not to build one stands on our own reasoning, not on someone else's words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary 2: two ways to fake scale, neither closed
&lt;/h2&gt;

&lt;p&gt;Not building a reputation score closes off one way to fake standing. Two separate, unrelated ways remain open, and we found both ourselves in an abuse scan on 2026-09-06 rather than fixed either.&lt;/p&gt;

&lt;p&gt;The first is identity: nothing in capacity-attest stops one operator from registering many addresses and looking like many different buyers or sellers. A 14-agent research pass the same day checked whether this is already covered elsewhere, ERC-8004, Google's AP2, Microsoft's Entra Agent ID, the x402 Foundation's own work, the Legal Context Protocol, and concluded that it is. What we shipped from that pass was small: externalRefs citation fields and a resolve_agent_identity tool, verified live against ERC-8004 on Base mainnet. Citing an ERC-8004 identity is not the same as stopping one operator from registering many of them, and we left this deliberately thin.&lt;/p&gt;

&lt;p&gt;The second is volume, a plainer problem with no identity layer involved. Nothing charges an actor for producing a claim, so an attacker can fabricate an unlimited stream of validly-signed, false claims against a real seller for free. get_delivery_history also returns results unbounded and synchronously, with no pagination and no rate limit on the read side. Both are marked in the decision log as found and not fixed, on purpose: rate-limiting one side while leaving the other free would look like a fix without being one. Both wait for an actual trigger, a real abuse case or a partner who needs the guarantee, before we commit to a specific mitigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026-09-06, 19:33 UTC: 0.5.0 ships, and gets proven on-chain
&lt;/h2&gt;

&lt;p&gt;Between the two pull requests, we published capacity-attest 0.5.0: cross-installation discovery plus an EAS source, commit c8fa192 ("chore(capacity-attest): 0.5.0 (cross-installation discovery + EAS source)").&lt;/p&gt;

&lt;p&gt;The problem it addresses is separate from either boundary above: get_delivery_history is local by default. If buyer A and buyer B each run their own installation, B does not automatically see the claim A recorded about the same seller. discoverDeliveryHistory pulls claims from multiple independent sources and re-verifies every one of them regardless of where it came from, trusting no source by default.&lt;/p&gt;

&lt;p&gt;We then proved this against a real public chain, not just a fixture. On Base mainnet, a schema was registered on EAS, UID 0x1dd19408345dee43b432b89ccb68760265ecff506098b6efe8ba82ad0d52b195, and two claims were published as attestations: one delivered yes, UID 0x81a55d54452b2cf8bdda7918f63a27bf9ff79e5025b485f7316aae6259288ccc, and one delivered no, UID 0xe736b005cbcb54f8f196ac64ef09d75d939c8a18c0d5d9670b5c5025c07398c4. Total cost for the three transactions was about 0.0000096 ETH, roughly 2.5 dollar cents. A separate read-only script, standing in for a fresh installation, then found both attestations on-chain and verified each one locally, including the negative one.&lt;/p&gt;

&lt;p&gt;This solves discoverability, not completeness. Finding a claim across installations and proving you found every claim that exists are different problems, and only the first one has a shipped fix. One honest caveat on the numbers above: those three UIDs are quoted directly from our decision log. We have not independently re-checked them against base.easscan.org as part of writing this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026-09-07, 04:54 UTC: EmbryoSpace merges PR #19
&lt;/h2&gt;

&lt;p&gt;The second builder took a different path, into the same asm-spec repository. EmbryoSpace's PR, 543 additions across 9 files, reuses only the claim format, claimId as a sha256 hash of canonical content. Nothing else.&lt;/p&gt;

&lt;p&gt;It could not reuse more, because our verifyClaim function is typed around ETH-style EIP-191 signatures. EmbryoSpace's claims are signed and encoded for BSV, base58, a different signature scheme entirely. Our verification code cannot check a BSV signature, so there was no version of this PR where it could call into our package the way YE-YI7's did.&lt;/p&gt;

&lt;p&gt;That is the actual answer to whether the format is rail-neutral. It is, in the one part that matters most: content-addressing. The claimId only depends on hashing canonical content, so any chain can produce and consume the identifier. The signature and its verification are not neutral at all, and pretending otherwise would have been the wrong thing to write in a decision log meant to be checked, not trusted.&lt;/p&gt;

&lt;p&gt;One more line item stayed unbuilt for a plainer reason: an in-transit status for multi-day deliveries. Neither external contact asked for it, and the closest real-world comparisons, AWS, Vast.ai, RunPod, also bill after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, plainly
&lt;/h2&gt;

&lt;p&gt;capacity-attest is at 0.5.0 on npm, with 646 downloads over the period 2026-08-08 through 2026-09-06. The repository has 0 stargazers, 0 forks, 1 open issue. The most recent commit, c8fa192, landed 2026-09-06T19:33:36Z, and the four commits before it, all from the same author, landed on the same calendar day. This is one person moving fast on one day, not a project carrying years of review.&lt;/p&gt;

&lt;p&gt;Exactly two people outside this project, YE-YI7 and EmbryoSpace, have built something that uses the claim format or the package itself, confirmed through their own merged pull requests, and they did it in two different ways worth telling apart. One calls into capacity-attest's own verification code. The other reuses only the hash-based identifier because its signature scheme cannot be checked by this code at all.&lt;/p&gt;

&lt;p&gt;One boundary, completeness of visibility across a claim history, got a real but partial fix on 2026-09-06: it catches a broken chain, not a fabricated silence. A second boundary, faking scale, splits into an identity gap that was named and researched but left deliberately thin, and a plainer volume gap, free claim fabrication and an unbounded read endpoint, found and documented instead of patched over. Two strangers proved the format holds across two different rails. The decision log is where the next person finds out what it still does not hold against, corrections included.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>crypto</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We had two independent proofs of a critical RCE. The real software disagreed.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:38:08 +0000</pubDate>
      <link>https://dev.to/holistis/we-had-two-independent-proofs-of-a-critical-rce-the-real-software-disagreed-20bd</link>
      <guid>https://dev.to/holistis/we-had-two-independent-proofs-of-a-critical-rce-the-real-software-disagreed-20bd</guid>
      <description>&lt;p&gt;Yesterday we found what looked like a critical remote code execution bug in Postman's script sandbox. Two of us reproduced it independently, both times with real command execution. We scored it, wrote most of the report, and started building the end-to-end proof of concept before reaching out to Postman's security team.&lt;/p&gt;

&lt;p&gt;Then we ran that proof of concept against the actual, currently published packages, and it failed. Both times.&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug we thought we had
&lt;/h2&gt;

&lt;p&gt;Postman lets you run JavaScript before and after a request, for things like generating auth tokens or checking a response. That script runs in a restricted environment: no &lt;code&gt;process&lt;/code&gt;, no &lt;code&gt;require&lt;/code&gt;, no filesystem. The restriction works by literally naming those as function parameters and calling the function with &lt;code&gt;undefined&lt;/code&gt; for each one, so any script that types &lt;code&gt;process&lt;/code&gt; gets &lt;code&gt;undefined&lt;/code&gt; instead of the real thing.&lt;/p&gt;

&lt;p&gt;There's a classic way around that trick. &lt;code&gt;Function('return this')()&lt;/code&gt; compiles a brand new function from a string and calls it, and that new function does not inherit any of the parameter shadowing from the function it was written inside. If it runs in the same JavaScript realm as the real &lt;code&gt;process&lt;/code&gt; object, it hands that object straight back to you.&lt;/p&gt;

&lt;p&gt;We tried it against a standalone rebuild of Postman's isolation logic (the two open source packages it's built on, &lt;code&gt;uvm&lt;/code&gt; and &lt;code&gt;uniscope&lt;/code&gt;) and got a real process object back, twice, independently. From there: &lt;code&gt;process.binding('spawn_sync')&lt;/code&gt;, and we had command execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fell apart
&lt;/h2&gt;

&lt;p&gt;A rebuild of the logic is not the same thing as the actual product. So before writing to Postman, we built the exploit again, this time against the real, currently published &lt;code&gt;postman-sandbox&lt;/code&gt;, &lt;code&gt;postman-runtime&lt;/code&gt;, and &lt;code&gt;newman&lt;/code&gt; packages, called the way an actual script would call them.&lt;/p&gt;

&lt;p&gt;Both attempts failed. The error from the &lt;code&gt;newman&lt;/code&gt; path was blunt: &lt;code&gt;Cannot read properties of undefined (reading 'mainModule')&lt;/code&gt;. The &lt;code&gt;process&lt;/code&gt; object we thought we'd recovered was undefined.&lt;/p&gt;

&lt;p&gt;It turns out Postman's real sandbox does something our rebuild didn't: before any user script runs, it deliberately strips &lt;code&gt;process&lt;/code&gt;, &lt;code&gt;Buffer&lt;/code&gt;, and &lt;code&gt;globalThis&lt;/code&gt; off the actual global object, a step it calls &lt;code&gt;recreatingTheUniverse()&lt;/code&gt;, documented in their changelog since &lt;code&gt;postman-sandbox&lt;/code&gt; v4.0.0. On the &lt;code&gt;newman&lt;/code&gt; path there's a second, independent layer on top of that: the script runs inside its own separate V8 context, so there is no shared realm for the trick to abuse in the first place.&lt;/p&gt;

&lt;p&gt;Our rebuild had faithfully reproduced the parameter-shadowing trick. It just hadn't reproduced the hardening Postman built on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're taking from this
&lt;/h2&gt;

&lt;p&gt;We keep a running rule for ourselves: an exploit only counts as reachable once it's been run against the real, unmodified code through its own public entry point, never against a rebuild of the mechanism, however faithful that rebuild feels. This is the second time we've had to learn a version of that lesson, and this time it cost us a wasted day rather than a wasted submission.&lt;/p&gt;

&lt;p&gt;No report went to Postman. Nothing was submitted, nothing was retracted. We're writing this up because the failure is more useful to share than the almost-bug was, and because "we double-checked our own work before it left the building" is the kind of thing you can only prove by actually doing it in public.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I counted how many CI checks cannot fail. On a 120-file sample, 74 percent.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Sat, 05 Sep 2026 11:11:51 +0000</pubDate>
      <link>https://dev.to/holistis/i-counted-how-many-ci-checks-cannot-fail-on-a-120-file-sample-74-percent-3g7k</link>
      <guid>https://dev.to/holistis/i-counted-how-many-ci-checks-cannot-fail-on-a-120-file-sample-74-percent-3g7k</guid>
      <description>&lt;p&gt;Last week I found two checks in my own repository that could never have failed. Not broken, not misconfigured. Structurally incapable of going red, sitting in CI for months, costing the same as a real check and reporting the same colour.&lt;/p&gt;

&lt;p&gt;So I went looking for how common that is. Here is the count, the method, and what it does not prove.&lt;/p&gt;

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

&lt;p&gt;GitHub code search, all public repositories, on 5 September 2026.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workflow files (containing runs-on in .github/workflows)   7,094,272
  of those, containing "continue-on-error: true"             264,704
  containing "npm run lint"                                  301,568
  containing BOTH "npm run lint" and "continue-on-error"      27,232
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is where it gets interesting, and also where code search stops being able to help. It tells you both strings live in the same file. It cannot tell you the &lt;code&gt;continue-on-error&lt;/code&gt; is attached to the lint step rather than to something unrelated three jobs down.&lt;/p&gt;

&lt;p&gt;So I read them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;p&gt;I pulled 500 matching files from pages spread across the result set, shuffled them, and inspected the first 120 by fetching each file and walking it step by step.&lt;/p&gt;

&lt;p&gt;Two things I had to fix in my own method before the number meant anything.&lt;/p&gt;

&lt;p&gt;The first pass gave 80 percent, and it was wrong. &lt;code&gt;path:.github/workflows&lt;/code&gt; also matches &lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;TROUBLESHOOTING.md&lt;/code&gt; and &lt;code&gt;ci.md&lt;/code&gt; sitting in that directory, plus &lt;code&gt;.yml.disabled&lt;/code&gt; files that do not run. Filtering to actual &lt;code&gt;.yml&lt;/code&gt; and &lt;code&gt;.yaml&lt;/code&gt; removed that noise.&lt;/p&gt;

&lt;p&gt;The second was a distinction I nearly missed. One of the early hits was a step called &lt;strong&gt;"Lint (non-blocking)"&lt;/strong&gt;. That is not the problem. That is somebody being explicit. Counting it alongside the silent ones would have been unfair and would have inflated the number.&lt;/p&gt;

&lt;p&gt;With both corrections, on 120 real workflow files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check step that silently cannot fail       89   (74%)
check step that says so in its name         3   (2.5%)
no toothless check step at all             28   (23%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security audits are the worst of it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workflow files running "npm audit"                          71,296
  of those, also containing "continue-on-error"             19,200   (27%)
workflow files with "npm test" and "continue-on-error"      22,592
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More than a quarter of the workflows that run a dependency audit also contain something allowed to fail. Given the 74 percent rate in the sample, most of those are the audit itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three shapes I verified by reading the file
&lt;/h2&gt;

&lt;p&gt;I am not linking these. They are small public projects and the point is the pattern, not the people. Run the search yourself and read the first ten results; you will find the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plain one.&lt;/strong&gt; A step named &lt;code&gt;Lint&lt;/code&gt;, running &lt;code&gt;npm run lint&lt;/code&gt;, with &lt;code&gt;continue-on-error: true&lt;/code&gt; directly under it. The name promises a check. Nothing in the Actions tab tells you it cannot fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one with the padlock.&lt;/strong&gt; A step named &lt;code&gt;🔐 Check for security vulnerabilities&lt;/code&gt;, running &lt;code&gt;npm audit --audit-level=moderate&lt;/code&gt;, with &lt;code&gt;continue-on-error: true&lt;/code&gt;. The emoji is doing more work than the check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The belt and braces.&lt;/strong&gt; A step named &lt;code&gt;Run tests (if available)&lt;/code&gt; whose command is &lt;code&gt;npm test || echo "No tests configured"&lt;/code&gt;, with &lt;code&gt;continue-on-error: true&lt;/code&gt; on top. The &lt;code&gt;|| echo&lt;/code&gt; already swallows the exit code. The flag catches whatever the first mechanism missed. That step cannot report a failure through two independent layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worse than having no check
&lt;/h2&gt;

&lt;p&gt;A missing check leaves you appropriately nervous. You know you are not covered.&lt;/p&gt;

&lt;p&gt;A check that cannot fail costs exactly the same to run, occupies the same line in the Actions tab, renders the same green, and buys you confidence it has not earned. It is not neutral. It actively replaces the nervousness that would have made you look.&lt;/p&gt;

&lt;p&gt;There is a second cost that surprised me. On my own suite, the twelve failing tests were 8 percent of the tests and 36 percent of the runtime, because a failing test burns its full timeout plus a retry. A dead check does not just fail to inform you. It also does not fail fast, because it never fails at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the flag is right
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;continue-on-error&lt;/code&gt; is a good feature and this is not an argument against it.&lt;/p&gt;

&lt;p&gt;An experimental matrix leg on a nightly toolchain, an optional platform, a step whose failure genuinely does not block the merge: all fine. Three of the files in my sample used it exactly that way and said so in the step name.&lt;/p&gt;

&lt;p&gt;The distinction is not the flag. It is whether the name tells the truth. &lt;code&gt;Lint (non-blocking)&lt;/code&gt; is honest. &lt;code&gt;Lint&lt;/code&gt; is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check I now run on myself
&lt;/h2&gt;

&lt;p&gt;Reading a check does not tell you whether it works. The only way to know is to introduce the exact defect it claims to catch and confirm it goes red.&lt;/p&gt;

&lt;p&gt;I ran that against ten checks in one repository. Two could never have fired. One of them is worth describing, because I would not have predicted it.&lt;/p&gt;

&lt;p&gt;The check verified that a &lt;code&gt;curl&lt;/code&gt; in a notification step carried the &lt;code&gt;--fail&lt;/code&gt; flag, by searching the step for the flag name. Directly above that &lt;code&gt;curl&lt;/code&gt; sat a comment explaining why the flag mattered. Remove the flag, leave the comment, and the check stayed green, because the string was still on the page.&lt;/p&gt;

&lt;p&gt;The more carefully I had documented why the flag was necessary, the more reliably I had disabled the check that enforced it.&lt;/p&gt;

&lt;p&gt;Three measurements per case, not one: green before the mutation, red during, green after. If any of the three is off, the result is unknown rather than pass. My own harness produced four unusable results before it produced a trustworthy one, every time because of the harness and not the checks. An exercise about detectors that cannot fire is a good place to remember that the detector you just wrote is also a detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this count does not prove
&lt;/h2&gt;

&lt;p&gt;Worth stating plainly, because a number without its limits is a claim dressed up as a fact.&lt;/p&gt;

&lt;p&gt;GitHub's code search totals are approximate. My sample came from the first thousand results rather than the full 27,232, so it is indicative and not a proper random estimate. I only measured the JavaScript ecosystem, through npm commands; Python, Go and Rust workflows are not in the count. My step parser is a heuristic that walks lines between step boundaries, and I hand-verified three of its hits rather than all 89.&lt;/p&gt;

&lt;p&gt;What I am confident saying: this is common, it is not rare, and security audits are disproportionately affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-minute version
&lt;/h2&gt;

&lt;p&gt;Open your workflows. Search for &lt;code&gt;continue-on-error: true&lt;/code&gt;. For each one, ask whether the job or step name promises something it cannot deliver.&lt;/p&gt;

&lt;p&gt;Then pick your most important check and break the thing it watches for. If it stays green, you did not have that check. You had a line item.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A CI timeout is a kill, not a failure. Playwright's defaults changed because of it.</title>
      <dc:creator>holistis</dc:creator>
      <pubDate>Sat, 05 Sep 2026 06:37:18 +0000</pubDate>
      <link>https://dev.to/holistis/a-ci-timeout-is-a-kill-not-a-failure-playwrights-defaults-changed-because-of-it-318j</link>
      <guid>https://dev.to/holistis/a-ci-timeout-is-a-kill-not-a-failure-playwrights-defaults-changed-because-of-it-318j</guid>
      <description>&lt;p&gt;Every morning there was a run in the Actions tab and nothing in my inbox. That is exactly what a healthy nightly looks like.&lt;/p&gt;

&lt;p&gt;It had not tested my app since late June. I found out in September.&lt;/p&gt;

&lt;p&gt;Here is what was happening, why nothing told me, and what changed in Playwright last week as a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is a kill, not a failure
&lt;/h2&gt;

&lt;p&gt;My nightly job had &lt;code&gt;timeout-minutes: 20&lt;/code&gt;. My suite had quietly grown past twenty minutes.&lt;/p&gt;

&lt;p&gt;When a GitHub Actions runner hits that limit it does not fail the job. It kills the process and records the run's conclusion as &lt;code&gt;cancelled&lt;/code&gt;. That single word is the whole problem, and it has three consequences that stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The report is never written.&lt;/strong&gt; Playwright is stopped mid-run. The reporters never get to flush, so there is no HTML report and no &lt;code&gt;results.json&lt;/code&gt;. Nothing on disk to look at afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The upload step skips itself.&lt;/strong&gt; The recommended workflow guards the artifact upload with &lt;code&gt;if: ${{ !cancelled() }}&lt;/code&gt;. That guard is right and it is still there. It exists so a run somebody cancelled by hand does not upload a half-finished report. The trouble is that a runner timeout produces the same conclusion as a hand cancellation, so the guard cannot tell them apart. You get no artifact on precisely the runs where you most want one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your alerting never fires.&lt;/strong&gt; If your notification step keys off the test step failing, it is not reached. A cancelled job skips everything downstream. No failed step, no email, no Slack message.&lt;/p&gt;

&lt;p&gt;So the run happens, the run is visible, and nothing about it says anything is wrong. It is indistinguishable from a night where the tests passed. Worse, it is indistinguishable from a run somebody cancelled by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers from my own repo
&lt;/h2&gt;

&lt;p&gt;I pulled the run history when I finally went looking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;164 nightly runs since June
  7 succeeded
 76 failed
 81 cancelled, the last 60-plus consecutively
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every cancelled run produced zero artifacts.&lt;/p&gt;

&lt;p&gt;Then I set &lt;code&gt;globalTimeout&lt;/code&gt; below the job timeout and ran the same suite. It finished in 29 minutes, reported &lt;code&gt;failure&lt;/code&gt; with 12 of 147 tests failing, and produced an 83 MB report.&lt;/p&gt;

&lt;p&gt;The suite had needed 29 minutes for a while. The 20 minute limit had been killing it nightly. Because a kill is not a failure, nothing said so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and why the ordering matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;globalTimeout&lt;/code&gt; is a Playwright setting. It defaults to no timeout at all.&lt;/p&gt;

&lt;p&gt;Set it below your job's &lt;code&gt;timeout-minutes&lt;/code&gt; and the order of events changes completely. Playwright stops itself first. It exits non-zero, the reporters finish, the HTML report and &lt;code&gt;results.json&lt;/code&gt; are written, and the run reads as an ordinary test failure. Your alerting fires because something actually failed.&lt;/p&gt;

&lt;p&gt;The runner timeout then only has to cover the install and upload steps around the run. It becomes a backstop instead of the primary bound.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Fail the run after an hour, so that the reporters still produce a report.&lt;/span&gt;
  &lt;span class="na"&gt;globalTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines. The entire difference between a loud failure and two months of silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How common this is
&lt;/h2&gt;

&lt;p&gt;Before filing anything I wanted to know whether this was my mistake or a shape lots of people were in.&lt;/p&gt;

&lt;p&gt;At the time I checked, GitHub code search reported roughly 163,000 &lt;code&gt;playwright.config.ts&lt;/code&gt; files. About 1,700 of them set &lt;code&gt;globalTimeout&lt;/code&gt;. That is around one percent.&lt;/p&gt;

&lt;p&gt;Around 3,200 workflows ran &lt;code&gt;npx playwright test&lt;/code&gt; on a cron schedule. That is the case where nobody is watching the run live, so a missing report costs the most.&lt;/p&gt;

&lt;p&gt;Those numbers are approximate and they move. The ratio is the part that mattered: the safe configuration was the rare one, and the docs never connected the two settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;I filed &lt;a href="https://github.com/microsoft/playwright/issues/42533" rel="noopener noreferrer"&gt;microsoft/playwright#42533&lt;/a&gt; on 3 September, proposing a note in &lt;code&gt;ci.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Playwright maintainer Dmitry Gozman went considerably further than the note I asked for, and merged two changes on 4 September.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The documentation&lt;/strong&gt;, in &lt;a href="https://github.com/microsoft/playwright/pull/42563" rel="noopener noreferrer"&gt;microsoft/playwright#42563&lt;/a&gt;, across four files. &lt;code&gt;ci.md&lt;/code&gt; now opens the CI section with this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always set a global timeout in CI. By default a test run has no upper bound, so a suite that hangs, or that slowly grows past the job limit of your CI provider, is killed by the runner mid-run and does not produce the test report.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it drops the job-level timeout from the workflow examples, with the reasoning attached: if you do add one, keep it comfortably above &lt;code&gt;globalTimeout&lt;/code&gt; so Playwright always stops first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scaffolding&lt;/strong&gt;, in &lt;a href="https://github.com/microsoft/create-playwright/pull/181" rel="noopener noreferrer"&gt;microsoft/create-playwright#181&lt;/a&gt;. This is the one that matters most. &lt;code&gt;create-playwright&lt;/code&gt; is what runs when anyone types &lt;code&gt;npm init playwright@latest&lt;/code&gt;. It now generates &lt;code&gt;globalTimeout: 60 * 60 * 1000&lt;/code&gt; in all four config templates, and no longer generates &lt;code&gt;timeout-minutes&lt;/code&gt; in the GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;I asked for a warning sign next to the hole. What landed was the hole being filled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you start a Playwright project from today&lt;/strong&gt;, you get the safe default and you do not need to think about any of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your project already exists&lt;/strong&gt;, you still have the old shape. The generated config sits in your repo from the day you created it and nothing rewrites it. Two things to check:&lt;/p&gt;

&lt;p&gt;Does your &lt;code&gt;playwright.config.ts&lt;/code&gt; set &lt;code&gt;globalTimeout&lt;/code&gt;? If not, your run has no upper bound of its own.&lt;/p&gt;

&lt;p&gt;Does your CI job set &lt;code&gt;timeout-minutes&lt;/code&gt;? If yes, and &lt;code&gt;globalTimeout&lt;/code&gt; is unset or higher, the runner is your only limit and it kills without reporting.&lt;/p&gt;

&lt;p&gt;There is also a check that takes ten seconds and tells you whether this already happened to you. Open your nightly workflow's run history and look at the conclusions. If you see a run of &lt;code&gt;cancelled&lt;/code&gt; you never investigated, that is this bug, and those nights were not tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep thinking about
&lt;/h2&gt;

&lt;p&gt;The bug was not that something broke. Something broke and the system that was supposed to tell me was structurally incapable of telling me, because the failure mode it produced was the one shape it treated as "nothing to report".&lt;/p&gt;

&lt;p&gt;I have since gone looking for more of those. In my own tooling I found a check that verified a &lt;code&gt;curl&lt;/code&gt; had the &lt;code&gt;--fail&lt;/code&gt; flag by searching the whole workflow step for the word. Directly above the &lt;code&gt;curl&lt;/code&gt; sat a comment explaining why the flag mattered. Remove the flag, leave the comment, and the check stays green.&lt;/p&gt;

&lt;p&gt;The more carefully I had documented why the flag was necessary, the more reliably I had disabled the check that enforced it.&lt;/p&gt;

&lt;p&gt;The way you find these is not by reading them. It is by introducing the exact defect each check claims to catch and confirming it goes red. I ran that against ten checks in one repo and two of them could never have failed. When I ran it, my own test harness turned out to be broken twice before the checks were.&lt;/p&gt;

&lt;p&gt;A check that cannot fail costs exactly as much to run as one that can, looks identical in the Actions tab, and buys you confidence it has not earned. That is worse than having no check, because no check at least leaves you appropriately nervous.&lt;/p&gt;

&lt;p&gt;Look at what your green means. Sometimes it means nothing went wrong. Sometimes it means nothing could have told you.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
