<?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: Marcin Dudek</title>
    <description>The latest articles on DEV Community by Marcin Dudek (@marcindudekdev).</description>
    <link>https://dev.to/marcindudekdev</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%2F3952428%2F7fa26691-9eac-410d-866a-036a2d4ecd6c.png</url>
      <title>DEV Community: Marcin Dudek</title>
      <link>https://dev.to/marcindudekdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcindudekdev"/>
    <language>en</language>
    <item>
      <title>33 yes/no questions cannot identify 8 billion people. Here are 77 that nearly do.</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Mon, 31 Aug 2026 08:35:16 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/33-yesno-questions-cannot-identify-8-billion-people-here-are-77-that-nearly-do-59ig</link>
      <guid>https://dev.to/marcindudekdev/33-yesno-questions-cannot-identify-8-billion-people-here-are-77-that-nearly-do-59ig</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/77-questions-identify-every-human/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/77-questions-identify-every-human/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In short&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 2013 GitHub repo asked for 33 yes/no questions that uniquely identify every person alive. It has 361 stars, 39 forks, and two questions. Nobody finished it.&lt;/li&gt;
&lt;li&gt;33 is the wrong number, and the README says why in the sentence right before it. Assigning 8 billion serial numbers costs 33 bits. Asking questions about facts people already have costs about 65, because those answers land pseudo-randomly and the birthday problem applies.&lt;/li&gt;
&lt;li&gt;At 33 perfect independent bits, 4.85 billion people still share their answer pattern with somebody.&lt;/li&gt;
&lt;li&gt;I built the list. It runs to 77 questions and 65.6 Shannon bits. That's good enough for "almost everyone" and not good enough for "everyone" - P(nobody in 8 billion collides) is about 0.57.&lt;/li&gt;
&lt;li&gt;Three models reviewed it adversarially and forced ten corrections, including one where all three independently told me the same block was worth 6.5 bits when I had 9.19. They were right.&lt;/li&gt;
&lt;li&gt;Identical twins named Anna and Anne answer 60 of the 77 identically, including all six given-name questions. No number of extra questions fixes that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In November 2013 someone opened a repo called &lt;a href="https://github.com/MarkDunne/33-questions" rel="noopener noreferrer"&gt;33-questions&lt;/a&gt; and wrote a very good README. It has 361 stars and 39 forks. It was last pushed on 25 November 2013, two days after it was created. The question list is still this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="mf"&gt;1.&lt;/span&gt; &lt;span class="k"&gt;Do&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;identify&lt;/span&gt; &lt;span class="n"&gt;yourself&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;male&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="mf"&gt;2.&lt;/span&gt; &lt;span class="k"&gt;Do&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;currently&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;following&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;China&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;India&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="nc"&gt;The&lt;/span&gt; &lt;span class="nc"&gt;United&lt;/span&gt; &lt;span class="nc"&gt;States&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Indonesia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Brazil&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="nc"&gt;Pakistan&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="mf"&gt;3.&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirteen years, nobody finished it. I spent a couple of days on it and I think I know why, and it isn't that the questions are hard to think of. The target was impossible from the first paragraph.&lt;/p&gt;

&lt;p&gt;The finished list, the full working with every correction marked in place, and the measurement scripts are here: &lt;a href="https://github.com/MarcinDudekDev/77-questions" rel="noopener noreferrer"&gt;MarcinDudekDev/77-questions&lt;/a&gt;. Every number below tagged as measured came out of those scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error is in two consecutive sentences
&lt;/h2&gt;

&lt;p&gt;Here is the README, quoted exactly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We could give everybody on the planet a unique series of 33 1s and 0s, and identify anyone by their personal series. But that would be boring. What if, instead assigning 1s and 0s, we had 33 'Yes' or 'No' general questions that, when answered correctly, uniquely identified everyone on the planet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are two different problems, and the second one costs about 32 bits more than the first.&lt;/p&gt;

&lt;p&gt;Assigning serial numbers is cheap because you control the assignment. You hand out 8 billion labels, each one used once, and log2(8×109) = 32.9 bits is enough. Nothing collides because you never let it.&lt;/p&gt;

&lt;p&gt;Nobody is assigning anything here. The answers fall out of facts people already have - their birthday, their name, their mother's birthday. Those land pseudo-randomly across the space of possible patterns, so two people can land on the same one, and you get the &lt;a href="https://en.wikipedia.org/wiki/Birthday_problem" rel="noopener noreferrer"&gt;birthday problem&lt;/a&gt; at planetary scale. With N people spread over M = 2b patterns, the expected number of colliding pairs is N²/2M. Setting that below 1 needs b = log2(N²/2) = 64.8 bits.&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%2Fmm156uutiq14epgt96qm.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%2Fmm156uutiq14epgt96qm.png" alt="Log-scale chart of how many of 8 billion people still share an answer pattern, against bits collected. At 33 bits, 4.85 billion. At 65.6 bits, about 1. At 71.4 bits, 0.02." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Expected number of people who still share their answers with somebody, N(1 − e−N/M), for N = 8 billion. The curve is flat until well past 33 bits, which is exactly the region the original repo was aiming at.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At 33 perfect, independent, 50/50 questions, 4.85 billion people out of 8 billion still share their answer pattern with somebody. 33 questions doesn't nearly work. It fails almost completely.&lt;/p&gt;

&lt;p&gt;The gap between 33 and 65 is the answer to the question the repo was asking. It isn't a detail you tune away with cleverer questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Unique" has three different prices
&lt;/h2&gt;

&lt;p&gt;This one I got wrong for three drafts, and a reviewer caught it by noticing I was quoting one metric and tabulating another. There are three thresholds and they're 6.6 bits apart:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Bits needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expected colliding pairs The last row is what "uniquely identified everyone on the planet" plainly means, and my finished list doesn't reach it. At its 65.6 bits, the probability that all 8 billion patterns are distinct is &lt;strong&gt;0.57&lt;/strong&gt;. A coin flip. Even at the 68.1 bits I claimed before the review, it was 0.90, so roughly one draw in ten still had a collision in it.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So "77 questions gets almost everyone" is true. "77 questions uniquely identifies every living human" is false, and it stays false under assumptions that are already too kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't ask your way past a fact
&lt;/h2&gt;

&lt;p&gt;Every question mines some underlying fact, and no set of questions about a fact can extract more bits than the fact contains. I measured the big one by enumerating every valid date from 1926 to 2026 and weighting it by an approximate world age distribution:&lt;/p&gt;

&lt;p&gt;H(your birth date) = 14.76 bits. That's a hard ceiling on every date question combined, however many you invent.&lt;/p&gt;

&lt;p&gt;That kills the obvious strategy. The original draft I started from wanted to keep adding date parities: day odd, month odd, day-of-year odd, sum of day and month even. You run into the wall at 14.76 and everything after that is worth nothing. It's also why the answer is never "just ask 65 date questions".&lt;/p&gt;

&lt;p&gt;Names have the same shape of limit. You can't ask about the 7th letter of a 4-letter name.&lt;/p&gt;

&lt;p&gt;The way past the ceiling turned out to be &lt;strong&gt;more people, not more cleverness about one person&lt;/strong&gt;. Your mother's birth day and month is a fresh 8.5-bit pool that your own birth date says nothing about. Twelve questions about both parents' birthdays take the colliding count from 8.6 million down to 2,674. Those twelve are worth more than the address, sibling and body-and-birth blocks put together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions worth exactly zero, and one I wrongly wrote off
&lt;/h2&gt;

&lt;p&gt;The fun part of this problem is that a question can look independent and be mathematically determined by two questions you already asked. I brute-forced the candidates instead of arguing about them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Is the sum of your birth day and month even?"&lt;/strong&gt; - zero bits, given day parity and month parity. Checked over all 372 day/month pairs, zero mismatches. All three of those questions were in the original draft's "best 20 to try first" list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Was your parent's age at your birth an even number?"&lt;/strong&gt; - zero bits. Given your own birth year and the parity of theirs, the parity of the gap is fixed. My selector scored it at +0.0000 and dropped it.&lt;/p&gt;

&lt;p&gt;Then the one I got backwards. I wrote off &lt;strong&gt;"do the digits of your birth year add up to an odd number?"&lt;/strong&gt; as a duplicate of the year-parity question and cut it. Measured over 1926-2026 the two are independent, mutual information 0.0009 bits, and the digit-sum question is worth a full bit. My greedy search picks it ninth. It's question 9 in the final list.&lt;/p&gt;

&lt;p&gt;The dependency is real, but it's conditional. Add a tens-digit question and the digit sum collapses: for 19xx the digit sum is 10 + tens + last, for 20xx it's 2 + tens + last, so its parity is exactly parity(tens) XOR parity(last), with zero exceptions over 1900-2099. That's what made two of my hand-written parents'-birth-year questions worth nothing, and it's why the birth-date block gets away with asking both - that block has no tens-digit question in it.&lt;/p&gt;

&lt;p&gt;Then a result I didn't expect. A-M letter cuts at different positions in a name are &lt;strong&gt;effectively independent&lt;/strong&gt;: mutual information between "1st letter is A-M" and "2nd letter is A-M" is 0.0045 bits, between 2nd and 3rd it's 0.0007. Vowel questions are not: "1st letter is a vowel" and "2nd letter is a vowel" share 0.278 bits. Names have strong phonotactic structure, and an A-M cut is coarse enough to average right over it while a vowel test sits directly on top of it. So the list uses A-M cuts everywhere and no vowel questions at all.&lt;/p&gt;

&lt;p&gt;I also measured the cost of insisting every question be answerable by a human without arithmetic. It's zero, within measurement noise. There's no tradeoff between rigour and readability here, which was a relief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I was wrong
&lt;/h2&gt;

&lt;p&gt;When the list was finished I handed it to three models - Grok, Sol running in Cursor, and Fable - and asked each to attack it. Between them they forced ten corrections. I'd rather write these down than quietly fold them in, partly because a couple of them are the kind of mistake I'd make again.&lt;/p&gt;

&lt;h3&gt;
  
  
  I introduced the exact error I'd criticised
&lt;/h3&gt;

&lt;p&gt;I spent a whole section of my notes complaining that the original draft contained zero-information questions. Then I hand-wrote the two parents'-birth-year blocks instead of running my own selector over them, and put in two questions worth exactly zero bits. The fix was to delete my hand-written block and let &lt;a href="https://github.com/MarcinDudekDev/77-questions/blob/main/greedy_parent.py" rel="noopener noreferrer"&gt;&lt;code&gt;greedy_parent.py&lt;/code&gt;&lt;/a&gt; pick, measuring each candidate's gain conditional on your own birth year already being known.&lt;/p&gt;

&lt;h3&gt;
  
  
  I budgeted in the wrong entropy
&lt;/h3&gt;

&lt;p&gt;I did the whole budget in Shannon entropy. Collisions don't depend on Shannon entropy. They depend on &lt;a href="https://en.wikipedia.org/wiki/R%C3%A9nyi_entropy" rel="noopener noreferrer"&gt;Rényi-2&lt;/a&gt;, the collision entropy, because expected colliding pairs = N²/2 × Σp² and Σp² = 2−H₂. H₂ ≤ H₁ always, with equality only for a perfectly uniform distribution, and names are the opposite of uniform.&lt;/p&gt;

&lt;p&gt;On the date block the gap is small: H₁ = 11.760 against H₂ = 11.588, because that block is nearly uniform by construction. On the name blocks it's brutal. A review estimate using real Chinese surname frequencies put one name block's collision entropy at about 4.25 bits against the 5.95 I'd credited, a 29% loss.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If the name blocks lose&lt;/th&gt;
&lt;th&gt;Effective bits&lt;/th&gt;
&lt;th&gt;People still colliding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0% (uniform, unreal)&lt;/td&gt;
&lt;td&gt;65.45&lt;/td&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;62.83&lt;/td&gt;
&lt;td&gt;6.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;60.21&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30% (the Chinese-surname figure)&lt;/td&gt;
&lt;td&gt;57.59&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the honest headline isn't "0.2 people still colliding", which is what I had. It's somewhere between about five and about a hundred and fifty, and the width of that range is set almost entirely by one thing I never obtained: real name-frequency data.&lt;/p&gt;

&lt;h3&gt;
  
  
  All three reviewers said the same number, and I argued
&lt;/h3&gt;

&lt;p&gt;I measured each parent's birth year in isolation. Mother 4.50 bits, father 4.69, credited as 9.19. Every one of the three reviewers came back independently with roughly 6.5 (Sol said 6.70, Fable about 6.5, Grok about 6) and pointed at the same thing: assortative mating. Parents' ages at your birth are correlated, fathers run about three years older, and measuring the two in isolation double-counts. Taken separately the two years have ceilings of 4.77 and 4.97 bits, so 9.74 between them. Model the age gap with a standard deviation of 2 to 4 years and the joint ceiling drops to 7.8-8.8. The budget now carries a 2.19-bit deduction for that overlap, which puts the pair at 7.00.&lt;/p&gt;

&lt;p&gt;They were right. What bothers me is the order I did it in: I only moved after measuring the joint myself. Three independent models converging on the same correction should have been enough to make me measure it &lt;em&gt;first&lt;/em&gt;, not to make me defend the number until I'd checked. I wrote about &lt;a href="https://marcindudek.dev/blog/one-shot-code-review-benchmark/" rel="noopener noreferrer"&gt;scoring models on restraint&lt;/a&gt; a few weeks ago, and this is the human version of the same failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two smaller ones
&lt;/h3&gt;

&lt;p&gt;I claimed my ordering was prefix-optimal, meaning any prefix of the list is the best identifier of that length, and I never tested it. It was false by up to 2.00 bits at the worst truncation point. Reordering so the parents' &lt;em&gt;date&lt;/em&gt; blocks come before the parents' &lt;em&gt;name&lt;/em&gt; blocks cuts that to 0.34 bits and costs nothing else.&lt;/p&gt;

&lt;p&gt;And I understated the identical-twin population twofold, by taking "0.35% of deliveries" and using it as "0.35% of people". A twin delivery produces two people. It's 0.70%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anna and Anne
&lt;/h2&gt;

&lt;p&gt;Sections one through five treat this as a budget problem. Buy enough bits and the collisions go away. They don't, and the reason isn't statistical.&lt;/p&gt;

&lt;p&gt;Take identical twin sisters. Same birth day, same parents, same address, same family name, same siblings, named Anna and Anne.&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%2F0myb6trzeyasou2d1kv9.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%2F0myb6trzeyasou2d1kv9.png" alt="Grid of 77 numbered dots. Sixty are dark, marking questions identical twins answer identically by construction. Seventeen are gold, marking the ones the list allows to differ." width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sixty of the seventy-seven are forced identical for a twin pair. Every high-value block is in the dark group.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The six given-name questions are the one thing in the list that should separate them, and they don't. Both names are 4 letters. Both start with A. Both have N as the 2nd and the 3rd letter. Both end in a letter in A-M. I checked all six mechanically and Anna and Anne produce the same answer to every single one. Sofia and Sonia differ on exactly one of the six. Anna and Alma differ on two.&lt;/p&gt;

&lt;p&gt;Adding more questions of the same kind cannot help, however many you add. That's the part of the bit budget that averages hide: 65 bits is enough on average, and the average is not where the problem lives. Identical twins are about 0.70% of people, so roughly &lt;strong&gt;56 million people in 28 million pairs&lt;/strong&gt;, all sitting in the one region of the answer space that generic questions never reach.&lt;/p&gt;

&lt;p&gt;The only questions that touch it are ones that target within-pair difference. That's why an otherwise 50/50 list ends with three questions split 2%, 1% and 45%. Question 76, "were you delivered first", is worth 0.081 bits averaged over everyone, which is nearly nothing. For the roughly 160 million people who were born as part of a multiple birth it's the most valuable question in the list, and for the 56 million identical twins among them it's one of only two or three that can separate a pair at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd trust and what I wouldn't
&lt;/h2&gt;

&lt;p&gt;The date measurements are solid - full enumeration of every valid date from 1926 to 2026, weighted by an approximate world age distribution. Changing the age weights moves the splits by fractions of a percentage point.&lt;/p&gt;

&lt;p&gt;The name-independence result uses &lt;strong&gt;English dictionary words as a proxy for names&lt;/strong&gt;, because I had no name-frequency corpus on the machine. The structural finding it supports, that coarse A-M cuts barely interact while vowel questions do, should survive a change of corpus. The exact splits for real names will not.&lt;/p&gt;

&lt;p&gt;Six of the sixteen blocks - both parents' given names, the mother's birth surname, the address, family structure and body - are &lt;strong&gt;estimated rather than measured&lt;/strong&gt;. Their discounts are reasoned guesses. The 54.41-bit total for the core 59 questions therefore carries maybe plus or minus 2 bits of real uncertainty, which moves the "2,674 people" figure by a factor of a few either way. It doesn't move the headline.&lt;/p&gt;

&lt;p&gt;And the whole thing assumes people answer correctly. At a 2% per-question error rate, the chance of getting all 77 right is 21%. In practice that's a bigger threat to this list than any correlation I've spent days measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 77 questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spelling rule for every name question:&lt;/strong&gt; use the spelling on your main government ID or birth certificate. No Latin form? Transliterate phonetically. Ignore hyphens, apostrophes and spaces. "First given name" and "family name" mean whichever your own culture treats as such. When a question asks about a letter your name is too short to have, answer &lt;strong&gt;no&lt;/strong&gt;. When you don't know, answer &lt;strong&gt;no&lt;/strong&gt;, and never substitute a fact from another question.&lt;/p&gt;

&lt;p&gt;They're grouped by where the answer comes from, and within that by how much each block is worth, so stopping early still leaves you holding the best questions you could have asked. Percentages are the share of people expected to answer yes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sex
&lt;/h3&gt;

&lt;p&gt;1 question · no lookup&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Do you identify as male? (under ~12: were you recorded male at birth?)&lt;/td&gt;
&lt;td&gt;50.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your age and birth date
&lt;/h3&gt;

&lt;p&gt;12 questions · your birth certificate&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Were you born in 1995 or later? (at or below the global median age, ~31 in 2026)&lt;/td&gt;
&lt;td&gt;53.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Were you born in January-March or July-September?&lt;/td&gt;
&lt;td&gt;49.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Is your day-of-year an odd number? (1 January = 1)&lt;/td&gt;
&lt;td&gt;50.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Were you born in January-June?&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Is your birth year an even number?&lt;/td&gt;
&lt;td&gt;50.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Is your birth day of the month the 16th or later?&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Is your birth day of the month in 8-15 or 24-31?&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Do the digits of your birth year add up to an odd number?&lt;/td&gt;
&lt;td&gt;49.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Were you born on a Monday, Tuesday or Wednesday?&lt;/td&gt;
&lt;td&gt;42.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Is your birth day of the month an odd number?&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Were you born on a Tuesday, Thursday or Saturday?&lt;/td&gt;
&lt;td&gt;42.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Is the last digit of your birth year 0, 1, 2, 3 or 4?&lt;/td&gt;
&lt;td&gt;50.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your given name
&lt;/h3&gt;

&lt;p&gt;6 questions · no lookup&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Does your first given name start with a letter A-M?&lt;/td&gt;
&lt;td&gt;51.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Is the 2nd letter of your first given name A-M?&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Is the 3rd letter A-M? (shorter name: answer no)&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;Is the 4th letter A-M? (shorter name: answer no)&lt;/td&gt;
&lt;td&gt;54.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;Is the last letter A-M?&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Does your first given name have an odd number of letters?&lt;/td&gt;
&lt;td&gt;49.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your family name
&lt;/h3&gt;

&lt;p&gt;6 questions · no lookup&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Does your family name start with a letter A-M?&lt;/td&gt;
&lt;td&gt;~51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;Is the 2nd letter of your family name A-M?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;Is the 3rd letter A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;Is the 4th letter A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~54%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Is the last letter A-M?&lt;/td&gt;
&lt;td&gt;~53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Does your family name have an odd number of letters?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your mother's birth day and month
&lt;/h3&gt;

&lt;p&gt;6 questions · ask your mother&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;Was your mother born in January-March or July-September?&lt;/td&gt;
&lt;td&gt;49.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;Is her day-of-year an odd number?&lt;/td&gt;
&lt;td&gt;50.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;Was she born in January-June?&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;Was she born on the 16th or later?&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;Was she born on a day in 8-15 or 24-31?&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;Was she born on an odd-numbered day of the month?&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your father's birth day and month
&lt;/h3&gt;

&lt;p&gt;6 questions · ask your father&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;Was your father born in January-March or July-September?&lt;/td&gt;
&lt;td&gt;49.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;Is his day-of-year an odd number?&lt;/td&gt;
&lt;td&gt;50.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;Was he born in January-June?&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;Was he born on the 16th or later?&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;Was he born on a day in 8-15 or 24-31?&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;Was he born on an odd-numbered day of the month?&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your mother's birth year
&lt;/h3&gt;

&lt;p&gt;5 questions · ask your mother&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;Is your mother's birth year an even number?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;Is her birth year divisible by 4, or 1 more than a multiple of 4?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Was she under 23, or 33 or older, when you were born?&lt;/td&gt;
&lt;td&gt;51.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;Is the last digit of her birth year 0, 1, 2, 3 or 4?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;Was she under 28 when you were born?&lt;/td&gt;
&lt;td&gt;51.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your father's birth year
&lt;/h3&gt;

&lt;p&gt;6 questions · ask your father&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;Is your father's birth year an even number?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;Is his birth year divisible by 4, or 1 more than a multiple of 4?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;Was he between 25 and 34 when you were born?&lt;/td&gt;
&lt;td&gt;45.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;Is the last digit of his birth year 0, 1, 2, 3 or 4?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;Was he under 28 when you were born?&lt;/td&gt;
&lt;td&gt;28.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;Is the tens digit of his birth year odd?&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your mother's given name
&lt;/h3&gt;

&lt;p&gt;4 questions · ask your mother&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;Does your mother's first given name start A-M?&lt;/td&gt;
&lt;td&gt;~51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Is its 2nd letter A-M?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;Is its 3rd letter A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;Is its last letter A-M?&lt;/td&gt;
&lt;td&gt;~53%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your father's given name
&lt;/h3&gt;

&lt;p&gt;4 questions · ask your father&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;Does your father's first given name start A-M?&lt;/td&gt;
&lt;td&gt;~51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;Is its 2nd letter A-M?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;Is its 3rd letter A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;Is its last letter A-M?&lt;/td&gt;
&lt;td&gt;~53%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your mother's birth surname
&lt;/h3&gt;

&lt;p&gt;4 questions · ask your mother&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;Does your mother's birth surname start A-M?&lt;/td&gt;
&lt;td&gt;~51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;Is its 2nd letter A-M?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;Is its 3rd letter A-M? (shorter or unknown: no)&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;Is its last letter A-M?&lt;/td&gt;
&lt;td&gt;~53%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Deeper into your own names
&lt;/h3&gt;

&lt;p&gt;4 questions · no lookup&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;Is the 5th letter of your first given name A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~58%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;Is the 6th letter of your first given name A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;Is the 5th letter of your family name A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~57%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;Is the 6th letter of your family name A-M? (shorter: no)&lt;/td&gt;
&lt;td&gt;~63%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Where you live
&lt;/h3&gt;

&lt;p&gt;4 questions · your address&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;Is your house or building number even?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;Is the last digit of your house or building number 0-4?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;Is the numeric part of your postal code even?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;Does your street name start with a letter A-M?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Family structure
&lt;/h3&gt;

&lt;p&gt;3 questions · your family&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;Do you have at least one older sibling by the same mother?&lt;/td&gt;
&lt;td&gt;~56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;Do you have at least one younger sibling by the same mother?&lt;/td&gt;
&lt;td&gt;~52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;td&gt;Did your mother bear an even number of children in total?&lt;/td&gt;
&lt;td&gt;~48%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Body and birth circumstance
&lt;/h3&gt;

&lt;p&gt;3 questions · yourself&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;Are you taller than the median adult of your sex in your country?&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;Were you born before noon, local time? (unknown: answer no)&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;Does your town or city of birth start with a letter A-M?&lt;/td&gt;
&lt;td&gt;~51%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Twin distinguishers
&lt;/h3&gt;

&lt;p&gt;3 questions · your birth record&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;Were you born as part of a multiple birth (twin, triplet or more)?&lt;/td&gt;
&lt;td&gt;~2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;If so, were you delivered first? (not a multiple: answer no)&lt;/td&gt;
&lt;td&gt;~1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;Was your birth weight above 3.2 kg / 7 lb? (unknown: answer no)&lt;/td&gt;
&lt;td&gt;~45%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  If you stop early
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stop after&lt;/th&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Narrows you to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;12.8&lt;/td&gt;
&lt;td&gt;1 in 7,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;24.7&lt;/td&gt;
&lt;td&gt;1 in 27 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;43.3&lt;/td&gt;
&lt;td&gt;1 in 11 trillion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;54.4&lt;/td&gt;
&lt;td&gt;1 in 24 million billion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;65.8&lt;/td&gt;
&lt;td&gt;everyone but a handful, and never identical twins&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those are counts of answer patterns, not of people. With 8 billion actual people the last row still leaves somewhere between a handful and a couple of hundred sharing a pattern with somebody, because real answers cluster. The 65.8 in that row is the straight sum of the blocks; the 65.6 I've been quoting is what's left after deducting the overlap between the sibling questions and the parents' birth years.&lt;/p&gt;

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

&lt;p&gt;Getting the general population to uniqueness is a budget problem, and 77 questions very nearly solves it. Getting &lt;em&gt;everyone&lt;/em&gt; there is a different problem, and no list of yes/no questions about inherited facts solves it, because the questions interrogate facts that twins share. That last step needs an attribute that's unique by design rather than by accident: a national ID, a phone number, a biometric hash. At which point you've stopped asking questions and started reading an identifier.&lt;/p&gt;

&lt;p&gt;Which is roughly what the original README said in its first sentence, before it talked itself out of it.&lt;/p&gt;

&lt;p&gt;Credit where it's due: the problem, and the framing that made it worth a weekend, come from&lt;br&gt;
&lt;a href="https://github.com/MarkDunne/33-questions" rel="noopener noreferrer"&gt;MarkDunne/33-questions&lt;/a&gt;.&lt;br&gt;
The numbers here are mine, and so are the mistakes the reviewers found in them. The working and the&lt;br&gt;
measurement scripts are at&lt;br&gt;
&lt;a href="https://github.com/MarcinDudekDev/77-questions" rel="noopener noreferrer"&gt;MarcinDudekDev/77-questions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>informationtheory</category>
      <category>research</category>
    </item>
    <item>
      <title>Revertix PRO review: staging, rollback and backups for one WordPress site</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Fri, 21 Aug 2026 12:29:12 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/revertix-pro-review-staging-rollback-and-backups-for-one-wordpress-site-5be3</link>
      <guid>https://dev.to/marcindudekdev/revertix-pro-review-staging-rollback-and-backups-for-one-wordpress-site-5be3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/revertix-pro-review/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/revertix-pro-review/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Revertix is a staging, rollback and backup plugin for WordPress. The free plugin makes a staging&lt;br&gt;
copy of your site, and the PRO add-on compares that copy against live, pushes selected changes&lt;br&gt;
back, rolls plugins and themes to earlier versions, and runs scheduled backups.&lt;/p&gt;

&lt;p&gt;I tested it over about three weeks: every feature, on a clean install, with a licence covering&lt;br&gt;
the full Agency tier. This review covers what it does well, where the limits are, two bugs I&lt;br&gt;
found and reported, and who the plugin suits.&lt;/p&gt;

&lt;p&gt;Short version: recommended for the staging-and-push loop on a single host, with two caveats&lt;br&gt;
about what it is not, and one piece of housekeeping if you have been running it since before&lt;br&gt;
version 1.0.2.&lt;/p&gt;

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

&lt;p&gt;Revertix is a staging and rollback tool for a single WordPress install. The free plugin creates&lt;br&gt;
a staging copy inside your existing site, at &lt;code&gt;yoursite.com/staging/&lt;/code&gt;. The PRO add-on compares&lt;br&gt;
that copy against live, pushes selected changes back, rolls plugins and themes to earlier&lt;br&gt;
versions, and runs scheduled backups.&lt;/p&gt;

&lt;p&gt;The word "sync" made me expect something it is not. Revertix does not connect two independent&lt;br&gt;
WordPress installations. There is no field for a remote host, an API key or credentials for a&lt;br&gt;
foreign site. The other side of every comparison is a staging copy Revertix itself made, picked&lt;br&gt;
by slug from its own registry.&lt;/p&gt;

&lt;p&gt;So if your staging lives at one host and production at another, this is not your tool. Reach&lt;br&gt;
for WP Migrate or WP Staging Pro. What Revertix does is the loop where you make a staging site&lt;br&gt;
next to production, work on it, and push the result back.&lt;/p&gt;

&lt;p&gt;That is a narrower promise than the marketing implies, and it is worth knowing before you pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is genuinely good
&lt;/h2&gt;

&lt;p&gt;The rollback is the best part, which is fitting for a plugin named after it. I took Akismet from&lt;br&gt;
5.7 down to 5.6. Three seconds, including an automatic snapshot of 5.7 taken before the switch.&lt;br&gt;
The confirmation dialog told me the truth in advance: a snapshot gets taken first, and the site&lt;br&gt;
may briefly enter maintenance mode. No surprises, no manual cleanup afterwards.&lt;/p&gt;

&lt;p&gt;Premium plugins get handled properly, and this is where most rollback tools go quiet. Point&lt;br&gt;
Revertix at something not on WordPress.org and instead of an empty version list you get a&lt;br&gt;
"Premium / custom" label, a sentence explaining why there is no public history, and a button to&lt;br&gt;
snapshot the current version so you have something to return to. It also snapshots premium items&lt;br&gt;
automatically once a day when their version changes.&lt;/p&gt;

&lt;p&gt;The safety backup before a push is real. I checked the archive: 4,048 files, 88 MB uncompressed.&lt;br&gt;
I killed one backup job halfway through archiving and the resulting zip was still complete. One&lt;br&gt;
caveat on that, from a bug I did not find myself, at the end of this review.&lt;/p&gt;

&lt;p&gt;The database footprint is the thing I did not expect. Revertix creates no tables of its own. Its&lt;br&gt;
options total a few hundred bytes and every single one has &lt;code&gt;autoload=off&lt;/code&gt;. I measured TTFB with&lt;br&gt;
the plugin on and off across twelve samples each, and the difference disappears into the noise.&lt;br&gt;
Plenty of plugins with a fraction of this feature set are heavier.&lt;/p&gt;

&lt;p&gt;Scheduled backups deserve a note. Six frequencies, a chosen hour, retention, and a per-area scope&lt;br&gt;
selector. More usefully, it is the only job in the plugin that finishes on its own. Every task&lt;br&gt;
you click in the interface is driven by polling from the open admin tab, so closing the tab&lt;br&gt;
strands the job mid-flight. I learned that by stranding one. The cron path has no such problem.&lt;/p&gt;

&lt;p&gt;Staging access control is layered properly: a login wall, &lt;code&gt;blog_public = 0&lt;/code&gt;, a&lt;br&gt;
&lt;code&gt;noindex, nofollow, noarchive&lt;/code&gt; tag that stays on even if you disable the wall, plus per-user and&lt;br&gt;
per-role allow lists. There is also a configurable admin bar colour on staging copies, which&lt;br&gt;
sounds trivial until you remember how people edit production by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bugs I found in 1.0.0
&lt;/h2&gt;

&lt;p&gt;Neither threw an error. In two days of testing I never saw a fatal, a white screen or a stack&lt;br&gt;
trace. WordPress never even created &lt;code&gt;debug.log&lt;/code&gt;. That is the part worth sitting with, because a&lt;br&gt;
crash at least tells you something went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The staging site could boot against the live database.&lt;/strong&gt; The wizard completed, the panel&lt;br&gt;
reported &lt;code&gt;Table prefix: wp_stgstaging_&lt;/code&gt;, the tables really were cloned. But the prefix swap in&lt;br&gt;
the generated &lt;code&gt;wp-config.php&lt;/code&gt; was done with a regular expression that only matched a quoted&lt;br&gt;
literal. My config declares the prefix through a function call, the pattern missed, and nothing&lt;br&gt;
checked whether the substitution had happened. The staging install came up pointing at&lt;br&gt;
production.&lt;/p&gt;

&lt;p&gt;I proved it rather than inferring it. I wrote a marker into the staging tables only, then&lt;br&gt;
requested a file served by the staging directory. It returned the live site's title. I changed&lt;br&gt;
one line in &lt;code&gt;wp-config.php&lt;/code&gt; by hand and the same URL returned the staging marker.&lt;/p&gt;

&lt;p&gt;The panel promises that nothing you do on staging touches the live site. In that state you got a&lt;br&gt;
second front end wired into production, and every test you ran there wrote to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file comparison saw nothing on the staging side.&lt;/strong&gt; I planted differences in five places so&lt;br&gt;
every bucket had something to show. The scan came back with one bucket: 4,045 files to remove&lt;br&gt;
from live. Zero modified, zero staging-only. That bucket was the entire installation.&lt;/p&gt;

&lt;p&gt;An actual push confirmed the consequence: &lt;code&gt;files: 0&lt;/code&gt;. The database moved across correctly, and a&lt;br&gt;
post that existed only on live survived, so row merging is additive and sane. Not one file moved.&lt;br&gt;
The feature sold as "file by file, line by line" synchronised only the database, and reported&lt;br&gt;
success while doing it.&lt;/p&gt;

&lt;p&gt;There was a third, smaller one. The licence server returned &lt;code&gt;expires_at&lt;/code&gt; as &lt;code&gt;2027&lt;/code&gt;, four digits,&lt;br&gt;
and the plugin read it as a Unix timestamp. As a timestamp, 2027 is January 1970, so&lt;br&gt;
&lt;code&gt;is_expired()&lt;/code&gt; returned true the moment you activated.&lt;/p&gt;

&lt;p&gt;I got that one half wrong in my own notes, so I will correct myself here. I wrote that it did not&lt;br&gt;
block updates, because I checked WordPress's &lt;code&gt;update_plugins&lt;/code&gt; transient and Revertix was absent&lt;br&gt;
from it whatever the licence date said. Revertix does not use WordPress.org. It has its own&lt;br&gt;
update endpoint, and the code that feeds results into WordPress returns early when&lt;br&gt;
&lt;code&gt;is_expired()&lt;/code&gt; is true. It did block updates, for every customer, and I had tested through the&lt;br&gt;
wrong door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reporting them, and what came back
&lt;/h2&gt;

&lt;p&gt;I sent the report on 9 August: two blocking bugs, one moderate, reproduction steps, line numbers,&lt;br&gt;
and an offer to retest. Version 1.0.1 shipped on 10 August with the staging database bug fixed.&lt;br&gt;
The file comparison lives in the PRO add-on and took longer, arriving on 21 August alongside&lt;br&gt;
1.0.2 of the free plugin.&lt;/p&gt;

&lt;p&gt;The fixes are not patches over the symptom. The prefix swap now verifies its own result, and&lt;br&gt;
where it cannot guarantee the separation the job stops with a clear message instead of building&lt;br&gt;
a staging site that lies to you. The expiry parser handles a timestamp, a bare year and an ISO&lt;br&gt;
date string, and migrates values written by the old version on read. The licence server was&lt;br&gt;
changed too, so it now sends a full date instead of four digits. Both ends, not just the cheap&lt;br&gt;
one.&lt;/p&gt;

&lt;p&gt;He also acted on two things I mentioned in passing and never filed as bugs. Backups, packages&lt;br&gt;
and the log now carry names nobody can guess, and every Revertix folder ships its own protection&lt;br&gt;
files instead of trusting the parent. On nginx, where &lt;code&gt;.htaccess&lt;/code&gt; is ignored, the plugin now&lt;br&gt;
tells you outright that your backup folder is exposed and hands you the rule to add. That was a&lt;br&gt;
remark at the bottom of my report and it came back as a feature.&lt;/p&gt;

&lt;p&gt;The changelog also describes the staging bug in the author's own words, which is a thing worth&lt;br&gt;
noticing on its own. Plenty of vendors would have written "improved staging reliability".&lt;/p&gt;

&lt;p&gt;I retested on the same install, with the same &lt;code&gt;wp-config.php&lt;/code&gt; that defeated the original regex.&lt;br&gt;
Fresh staging site: correct prefix, reads its own tables. Changeset: two modified, one&lt;br&gt;
live-only, one staging-only, each planted file in the right bucket. Push: three files moved,&lt;br&gt;
verified on disk.&lt;/p&gt;

&lt;p&gt;Buying a plugin is partly a bet on the person behind it, and this is the part of the test you&lt;br&gt;
cannot run yourself before you pay. A fix at the root the day after the report, the second&lt;br&gt;
blocker eleven days later, and two unfiled remarks picked up along the way. Worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you already run it, redo your backups
&lt;/h2&gt;

&lt;p&gt;This one is not mine. I found it in the 1.0.2 changelog while checking dates, and it matters&lt;br&gt;
enough that I am not going to bury it.&lt;/p&gt;

&lt;p&gt;Before 1.0.2, every &lt;code&gt;%&lt;/code&gt; character in your content was mangled inside exported databases and&lt;br&gt;
backups. Percent-encoded values took the worst of it: any slug generated from a non-Latin title,&lt;br&gt;
any encoded URL, growing to several times its length. An import could fail outright. The author's&lt;br&gt;
own instruction is to take a fresh export or backup, because anything made before that version&lt;br&gt;
carries the damage.&lt;/p&gt;

&lt;p&gt;So the archive I praised earlier was structurally sound, and I verified that it was, but I was&lt;br&gt;
verifying the wrong property. A zip can be complete and still hold corrupted rows. If you have&lt;br&gt;
been running Revertix and have backups from before 1.0.2 sitting on a disk somewhere, they are&lt;br&gt;
not the safety net you think they are. Take fresh ones. The same changelog says staging sites&lt;br&gt;
created before the fix should be deleted and re-created, which is the other half of the same&lt;br&gt;
housekeeping.&lt;/p&gt;

&lt;p&gt;Two things about that. It is a real hit, and anyone weighing this plugin should know before they&lt;br&gt;
buy. It is also exactly what a changelog is for, written plainly enough that a non-technical&lt;br&gt;
reader can act on it. I would rather have a vendor who tells me my old backups are damaged than&lt;br&gt;
one who quietly fixes it and hopes nobody looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;Recommended, without the caveat I was expecting to write.&lt;/p&gt;

&lt;p&gt;If you run the staging-and-push loop on one host, this does the job, and the parts around the&lt;br&gt;
edges are better than the category average. The confirmation dialogs tell you what is about to&lt;br&gt;
happen. The autoload discipline suggests someone who has cleaned up after other people's&lt;br&gt;
plugins. The rollback handles the premium case that most tools ignore.&lt;/p&gt;

&lt;p&gt;Three things still deserve a sentence. It is not a two-server migration tool, whatever "sync"&lt;br&gt;
suggests. Interactive jobs need the admin tab left open, so start a big push and go and do&lt;br&gt;
something else in another window. And if you were running it before 1.0.2, do the housekeeping&lt;br&gt;
in the section above before you trust anything you backed up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Lite is $9 once for eleven modules on unlimited sites. PRO runs $79 a year for one site, $199 a&lt;br&gt;
year unlimited, or $499 once. For an agency running the staging-and-push loop across client&lt;br&gt;
sites, the unlimited annual tier is the one that makes arithmetic sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not test
&lt;/h2&gt;

&lt;p&gt;Pull in the other direction, importing a package into a separate install, and the end-to-end&lt;br&gt;
update flow from inside wp-admin. I also could not tell you how many real hosting setups the&lt;br&gt;
original prefix bug would have hit. My &lt;code&gt;wp-config.php&lt;/code&gt; declares the prefix through a function&lt;br&gt;
call, which is what exposed it, and I did not survey what other hosts generate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;Nobody asked for this review and nobody saw it before publication. I sent the author a full bug&lt;br&gt;
report with reproduction steps and line numbers before writing it, and I offered to retest the&lt;br&gt;
fixes on the same environment. He shipped them, I retested, and this post says so.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>pluginreview</category>
    </item>
    <item>
      <title>What WordPress 7.1 Actually Changed</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:41:13 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/what-wordpress-71-actually-changed-e05</link>
      <guid>https://dev.to/marcindudekdev/what-wordpress-71-actually-changed-e05</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/wordpress-7-1-what-actually-changed/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/wordpress-7-1-what-actually-changed/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;WordPress 7.1 landed on 19 August. I went through it the way I go through any release that matters to my work: I downloaded the 7.0.4 and 7.1 zips from wordpress.org and diffed them. Not the changelog, the actual files.&lt;/p&gt;

&lt;p&gt;Three things came out of that worth writing down. One of them I haven't seen anyone mention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core has an API for AI agents, and 7.1 added a way to answer for it
&lt;/h2&gt;

&lt;p&gt;The Abilities API is not new in 7.1. It's &lt;code&gt;@since 6.9.0&lt;/code&gt;, and all four of its classes ship in 7.0.4 already. If you read somewhere that 7.1 introduced an AI API, that's wrong.&lt;/p&gt;

&lt;p&gt;What 7.1 added is the layer around it. Four things, none of which appear anywhere in 7.0.4:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;wp_ability_invoked&lt;/code&gt; - an action that fires on every ability call, before input normalization, so it sees the raw input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wp_pre_execute_ability&lt;/code&gt; - a filter that can short-circuit the whole call&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WP_Filter_Sentinel&lt;/code&gt; - a new final class, more on it below&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;public&lt;/code&gt; flag on abilities, defaulting to false&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the docblock for that &lt;code&gt;public&lt;/code&gt; flag, straight out of &lt;code&gt;class-wp-ability.php&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether the ability is meant to be available to clients such as the REST API, MCP, or AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MCP is the Model Context Protocol - the thing Claude and other agents use to talk to tools. It's now named in WordPress core source. That alone is worth knowing about.&lt;/p&gt;

&lt;p&gt;The part that got my attention is &lt;code&gt;wp_pre_execute_ability&lt;/code&gt;. Core documents what it does, and it's blunt about it. Returning anything other than the default value bypasses "input normalization, input validation, permission checks, the registered execute callback, output validation, and the surrounding actions".&lt;/p&gt;

&lt;p&gt;Permission checks are in that list.&lt;/p&gt;

&lt;h3&gt;
  
  
  I tested it
&lt;/h3&gt;

&lt;p&gt;I set up WordPress 7.1 locally and ran this against &lt;code&gt;core/get-site-info&lt;/code&gt;, which is one of the three abilities core registers itself. It requires &lt;code&gt;manage_options&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run as nobody.&lt;/span&gt;
&lt;span class="nf"&gt;wp_set_current_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'core/get-site-info'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_pre_execute_ability'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'core/get-site-info'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'HIJACKED BY FILTER'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="no"&gt;WITHOUT&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;DENIED&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ability_invalid_permissions&lt;/span&gt;
&lt;span class="no"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;RETURNED&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"HIJACKED BY FILTER"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same call, same anonymous user. Without the filter the permission callback denies it. With the filter hooked, the caller gets whatever the filter decided to return, and the permission callback never gets consulted.&lt;/p&gt;

&lt;p&gt;Now, the honest part. This is not a vulnerability and I'm not going to dress it up as one. The filter runs server-side, so anything that can hook it is already PHP running as your site. The REST endpoints have their own &lt;code&gt;permission_callback&lt;/code&gt;, and listing abilities requires &lt;code&gt;current_user_can( 'read' )&lt;/code&gt;. I checked that specifically, because a wrong security claim about core would be worse than saying nothing.&lt;/p&gt;

&lt;p&gt;Core also documents the intended uses, and they're reasonable: cached responses, rate limiting, maintenance mode, test mocking. Every one of those needs to run before the expensive work.&lt;/p&gt;

&lt;p&gt;What I'd want people to understand is the shape of it. Any plugin on the site can answer on an agent's behalf, silently, and the ability's own permission logic won't run. If you're building abilities and relying on &lt;code&gt;permission_callback&lt;/code&gt; as your security boundary, that boundary holds against REST callers and not against other PHP on the same install. That distinction matters more as more sites start exposing abilities to agents.&lt;/p&gt;

&lt;p&gt;It's the same trust model as the plugin installer filter I wrote about earlier this month. Different screen, same idea: a filter with no gate of its own, sitting on a path you assumed was guarded.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sentinel is a nice bit of engineering
&lt;/h3&gt;

&lt;p&gt;Small thing, but I liked it. &lt;code&gt;WP_Filter_Sentinel&lt;/code&gt; is a final class with an empty body. That's the whole implementation.&lt;/p&gt;

&lt;p&gt;It solves a problem WordPress has had forever. &lt;code&gt;apply_filters( 'something', null )&lt;/code&gt; can't tell you whether a callback returned &lt;code&gt;null&lt;/code&gt; deliberately or whether nothing was hooked at all. Both look identical on the way out. Every plugin author has hit this and worked around it with a magic string or a global.&lt;/p&gt;

&lt;p&gt;Core's fix is to pass a unique object as the default and compare with &lt;code&gt;===&lt;/code&gt; afterwards. If you get the same instance back, nobody touched it. Any other value, including &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, is a real answer from a real callback.&lt;/p&gt;

&lt;p&gt;I'd like to see this used more widely than one filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  A performance decision from 5.8 got reversed
&lt;/h2&gt;

&lt;p&gt;This one I found by diffing &lt;code&gt;wp-includes/media.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In 7.0.4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$infinite_scrolling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'media_library_infinite_scrolling'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 7.1 the default moved into a variable, and the docblock above it says: &lt;code&gt;@since 7.1.0 Changed default to true and introduced per-user opt-out of infinite scrolling&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Infinite scrolling in the Media Library is on again. It was switched off in 5.8, and performance was one of the stated reasons alongside accessibility and usability.&lt;/p&gt;

&lt;p&gt;I checked what a real install computes rather than trusting the diff. On a clean 7.1 site, with no filter of my own, the value that reaches the media JavaScript is &lt;code&gt;infiniteScrolling = 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you run sites with tens of thousands of attachments, this is the 7.1 change most likely to make wp-admin feel worse. Infinite scrolling replaces the pager, so the grid keeps requesting the next page of attachments as you scroll instead of stopping at one screenful. I verified the default is on, but I haven't measured this against a library with 100,000 attachments, so treat the size of the effect as unmeasured. There's a per-user opt-out and the &lt;code&gt;media_library_infinite_scrolling&lt;/code&gt; filter still wins, so it's fixable, but it's on by default now and most people won't know it changed.&lt;/p&gt;

&lt;p&gt;I don't have a strong opinion on whether the 5.8 decision or the 7.1 decision is right. I'd just want to see the numbers that justified the reversal, and I couldn't find any.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one performance item on the roadmap didn't ship
&lt;/h2&gt;

&lt;p&gt;The 7.1 roadmap said that when core detects both an object cache and a page cache, speculative loading eagerness would move from &lt;code&gt;conservative&lt;/code&gt; to &lt;code&gt;moderate&lt;/code&gt;. That was the performance item for this release.&lt;/p&gt;

&lt;p&gt;I checked &lt;code&gt;wp-includes/speculative-loading.php&lt;/code&gt; in the shipped zip. The defaults are still &lt;code&gt;prefetch&lt;/code&gt; and &lt;code&gt;conservative&lt;/code&gt;. The auto-detection isn't there.&lt;/p&gt;

&lt;p&gt;Then I loaded the front page of a 7.1 site and read the rules it actually prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"speculationrules"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prefetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eagerness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;conservative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conservative, as shipped.&lt;/p&gt;

&lt;p&gt;What did ship is a pair of overrides, &lt;code&gt;WP_SPECULATIVE_LOADING_DEFAULT_MODE&lt;/code&gt; and &lt;code&gt;WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS&lt;/code&gt;, settable as constants or environment variables.&lt;/p&gt;

&lt;p&gt;Those work. I put &lt;code&gt;WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS&lt;/code&gt; into &lt;code&gt;wp-config.php&lt;/code&gt;, set it to &lt;code&gt;moderate&lt;/code&gt;, reloaded, and the printed rule changed to &lt;code&gt;"eagerness":"moderate"&lt;/code&gt;. Removing the constant put it back to conservative. So a host can flip a whole fleet without shipping an mu-plugin, and the default for everyone else didn't move.&lt;/p&gt;

&lt;p&gt;Worth thinking about if you're on managed hosting. A host that sets eager prerender on an origin without a page cache multiplies the work that origin does, and you won't see it in your own config.&lt;/p&gt;

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

&lt;p&gt;Some numbers from the two zips, because they explain the shape of this release better than any feature list.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;7.0.4 to 7.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core, unpacked&lt;/td&gt;
&lt;td&gt;91 MB to 114 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wp-includes/js/dist&lt;/td&gt;
&lt;td&gt;28 MB to 50 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files in dist&lt;/td&gt;
&lt;td&gt;133 to 133&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;editor.min.js&lt;/td&gt;
&lt;td&gt;1021 KB to 1536 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;option.php&lt;/td&gt;
&lt;td&gt;3 lines changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache.php&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;class-wp-object-cache.php&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Core grew by a quarter and every byte of it is JavaScript. The file count in &lt;code&gt;dist&lt;/code&gt; didn't change at all, so nothing new was added there - the existing bundles got bigger. The new script modules that came with it are &lt;code&gt;vips&lt;/code&gt; and &lt;code&gt;video-conversion&lt;/code&gt;, which is image and video processing moving into the browser.&lt;/p&gt;

&lt;p&gt;On the server side, the things I care about most didn't move. &lt;code&gt;option.php&lt;/code&gt; changed by three lines and one of them is a PHPStan annotation. The object cache files are untouched. There's no autoload story in this release, no new indexes, nothing in the query layer. &lt;code&gt;class-wpdb.php&lt;/code&gt; has 157 changed lines and they're almost all PHP 8.5 deprecation fixes.&lt;/p&gt;

&lt;p&gt;Core also published no 7.1-versus-7.0 numbers for TTFB, LCP or query count. There's no performance field guide for this release, which there was for 6.2 through 6.9. So if you read that 7.1 is faster, ask where that came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the extra 23 MB goes, and who pays for it
&lt;/h2&gt;

&lt;p&gt;The size jump is the thing I most wanted to pin down, so I measured instead of guessing. All of this is from a real 7.1 install, not from the zip.&lt;/p&gt;

&lt;p&gt;The two new script modules are where the weight sits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vips/worker.min.js&lt;/td&gt;
&lt;td&gt;13,417,612 B (12.8 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;video-conversion/worker.min.js&lt;/td&gt;
&lt;td&gt;409,786 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vips/loader.min.js&lt;/td&gt;
&lt;td&gt;75 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;video-conversion/loader.min.js&lt;/td&gt;
&lt;td&gt;87 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Core's dev note said the worker is around 13 MB. It is, and now that's measured rather than repeated.&lt;/p&gt;

&lt;p&gt;The loaders being 75 and 87 bytes is the answer to the obvious worry. Those workers are fetched when an upload needs them, not when a screen loads.&lt;/p&gt;

&lt;h3&gt;
  
  
  The front end doesn't see any of it
&lt;/h3&gt;

&lt;p&gt;I loaded the front page of a default 7.1 site and totalled every asset it pulls. One file, 3 KB. No vips, no video conversion, nothing from the editor bundles. The 22 MB that &lt;code&gt;js/dist&lt;/code&gt; gained is admin-side code, and visitors don't download it.&lt;/p&gt;

&lt;p&gt;So if you were worried that upgrading makes your visitors download a bigger site, they don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admin is a different story, and there's one thing that shouldn't be there
&lt;/h3&gt;

&lt;p&gt;I logged into wp-admin and recorded every network response on three screens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Screen&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Downloaded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Users list&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;372 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media library&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;488 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New post&lt;/td&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;td&gt;1.1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 12.8 MB worker never appeared on any of them, which is what you want.&lt;/p&gt;

&lt;p&gt;What did appear on all three is &lt;code&gt;upload-media.min.js&lt;/code&gt;, 16 KB, including on the users list where there is nothing to upload.&lt;/p&gt;

&lt;p&gt;I pushed on that one. I made a subscriber account, confirmed with &lt;code&gt;user_can()&lt;/code&gt; that it has no &lt;code&gt;upload_files&lt;/code&gt; capability, logged in as them, and loaded their own profile page. The 16 KB media upload script downloads there too.&lt;/p&gt;

&lt;p&gt;It isn't a deliberate enqueue. &lt;code&gt;wp-upload-media&lt;/code&gt; is registered as a dependency of &lt;code&gt;wp-block-editor&lt;/code&gt;, &lt;code&gt;wp-block-library&lt;/code&gt; and &lt;code&gt;wp-editor&lt;/code&gt;, and &lt;code&gt;wp-block-library&lt;/code&gt; gets pulled into admin screens broadly. So the dependency graph drags it along, and nobody wrote a capability check because nobody wrote an enqueue.&lt;/p&gt;

&lt;p&gt;16 KB is not going to hurt anyone. I'm noting it because a script for uploading media has no business being downloaded by an account that cannot upload media, and that pattern tends to grow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Class loading: there's no autoloader to regress
&lt;/h3&gt;

&lt;p&gt;I went looking for a PSR-4 mapping or an autoloader change, because that would be the interesting failure mode. WordPress core has neither. There's no &lt;code&gt;spl_autoload_register&lt;/code&gt; anywhere in &lt;code&gt;wp-settings.php&lt;/code&gt; or &lt;code&gt;load.php&lt;/code&gt;, and no autoloader class. Core loads by 311 unconditional &lt;code&gt;require&lt;/code&gt; statements, the same way it always has.&lt;/p&gt;

&lt;p&gt;7.1 adds to that list. The four Abilities API classes, &lt;code&gt;abilities-api.php&lt;/code&gt;, &lt;code&gt;icons.php&lt;/code&gt; and the filter sentinel come to about 100 KB of PHP that every request reads, front end included, whether or not anything on the page uses an ability or an icon.&lt;/p&gt;

&lt;p&gt;So I measured what that costs. Same site, same content, same machine, upgraded and downgraded between the two versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;7.0.4&lt;/th&gt;
&lt;th&gt;7.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Included files&lt;/td&gt;
&lt;td&gt;697&lt;/td&gt;
&lt;td&gt;712&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP bytes included&lt;/td&gt;
&lt;td&gt;19,587,285&lt;/td&gt;
&lt;td&gt;20,091,659&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak memory&lt;/td&gt;
&lt;td&gt;53,805,056 B&lt;/td&gt;
&lt;td&gt;53,805,056 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fifteen more files and 2.5% more PHP read per request, and peak memory came out identical to the byte.&lt;/p&gt;

&lt;p&gt;TTFB I couldn't separate from noise. 7.1 ran a median of 41 ms over 20 requests with a 7 ms standard deviation, against 39 ms on 7.0.4. My first read of that used 8 samples and looked like a 10 ms regression, which is exactly the kind of number I'd rather not publish. With more samples it's noise. This is one local Docker install, so treat it as "I found no measurable slowdown here", not as a benchmark of your hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the security side, nothing
&lt;/h2&gt;

&lt;p&gt;I looked, because a fresh release is a reasonable place to look. 7.1 is not a security release. No CVE is assigned against it, WPScan lists nothing, and every fix it carries came from 7.0.2, 7.0.3 and 7.0.4 before it went final.&lt;/p&gt;

&lt;p&gt;The 7.0 line is where the interesting stuff happened. &lt;code&gt;CVE-2026-65640&lt;/code&gt;, fixed in 7.0.4, was an Author-level remote code execution through a PostScript upload on servers running Imagick and Ghostscript. Earlier there was a worse pair: an SQL injection in &lt;code&gt;author__not_in&lt;/code&gt; chained with a REST batch route confusion into unauthenticated RCE. Both of those are in CISA's Known Exploited Vulnerabilities catalogue, which means they were being used against real sites.&lt;/p&gt;

&lt;p&gt;If you're on 7.1, none of it is open. If you're sitting on 7.0.3 or earlier, the Imagick one is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading
&lt;/h2&gt;

&lt;p&gt;I'd take 7.1. The security position is better than any earlier 7.0, and nothing I found in the diff makes a front end slower in a way I could measure.&lt;/p&gt;

&lt;p&gt;Two things to do after: turn Media Library infinite scrolling back off if you have a big library, and check whether your host has opinions about those new speculative loading constants.&lt;/p&gt;

&lt;p&gt;The abilities work is what I'll be watching. Core is building a surface for autonomous agents, and the first piece of plumbing it added lets any plugin stand in front of that surface. I'd like to see what plugin authors do with it before deciding whether that's a problem.&lt;/p&gt;

&lt;p&gt;One personal note. My wordpress.org username shows up in the 7.1 credits, in the props list, which is a first for me - I checked 6.0 through 7.0 and it isn't in any of them. Small thing, and it's a props credit rather than anything grand, but I've been reading this codebase for fifteen years and it's nice to be a line in it.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>core</category>
    </item>
    <item>
      <title>Who Edits Your WordPress Add New Screen</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:52:08 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/who-edits-your-wordpress-add-new-screen-513k</link>
      <guid>https://dev.to/marcindudekdev/who-edits-your-wordpress-add-new-screen-513k</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/who-edits-your-add-new-screen/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/who-edits-your-add-new-screen/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I typed "zero blocks given" into Plugins › Add New and my own plugin wasn't the first result. It's called Zero Blocks Given. Nothing else on wordpress.org is called that. The search matched keywords and put something else above the exact name, which is the sort of small thing that stops being small once you've noticed it.&lt;/p&gt;

&lt;p&gt;So the thought was: is it possible to fix that screen, so the search actually works by name and not by keyword? WordPress is filterable nearly everywhere, so I figured there'd be a hook that would let me re-rank my own results.&lt;/p&gt;

&lt;p&gt;There is. It's called &lt;code&gt;plugins_api_result&lt;/code&gt;, and it lives in &lt;code&gt;wp-admin/includes/plugin-install.php&lt;/code&gt;. When you open Add New, WordPress asks the wordpress.org API for a list of plugins. Before that list reaches your screen, core hands it to this filter, and any active plugin on your site gets to edit it - reorder it, add to it, remove from it. There's no capability check on the filter, because filters don't have capability checks. The plugin already runs as your site.&lt;/p&gt;

&lt;p&gt;That was the fix I wanted. Then a second thought arrived: if this hook lets me reorder the list, it lets anything already installed on the site do the same, and I had no idea whether anything did.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I looked
&lt;/h2&gt;

&lt;p&gt;I scanned every free plugin and theme on WordPress.org - 63,619 plugins and roughly 13,000 themes - for code that touches the Add New screen.&lt;/p&gt;

&lt;p&gt;One caveat before any of this means anything. The wp.org directory is the slice of the ecosystem that goes through human review and has the strongest incentive to behave. Whatever I found there is a floor. Commercial builds, marketplace themes, nulled copies, anything fetched after install - I couldn't see any of it, so I claim nothing about it. This is not a prevalence rate for WordPress.&lt;/p&gt;

&lt;p&gt;Most of what came back was routine. Licence updaters answering for their own slug, plugins adding their own tab, vendors putting their own products on top of the list while leaving everything wp.org sent underneath. Self-promotion, in the open, harming nobody. I expected the story to end there.&lt;/p&gt;

&lt;p&gt;Two didn't fit that shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that deletes
&lt;/h2&gt;

&lt;p&gt;Meta Box has 500,000 installs. Here is the entire file, &lt;code&gt;meta-box/src/FeaturedPlugins.php&lt;/code&gt;, version 5.13.1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeaturedPlugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'plugins_api_result'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'process'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$tab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$tab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'featured'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'recommended'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'query_plugins'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$plugin&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'secure-custom-fields'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Featured or Recommended tab on a site running Meta Box, and Secure Custom Fields - the ACF fork that wordpress.org itself publishes - is gone from the list. The scope is exactly what the code says and no more: those two tabs, not search. You can still search for SCF and install it. But on the tabs, it's removed, there's no setting to stop it, and the readme doesn't mention it.&lt;/p&gt;

&lt;p&gt;Slim SEO, 70,000 installs, ships the same file with the same logic, differing only in the namespace and how it's wired up. Same vendor - eLightUp. A missing list entry is invisible by definition. You don't get a gap where SCF should have been; you get a list that looks complete. You'd only catch it by comparing against a clean install, which nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that displaced Featured
&lt;/h2&gt;

&lt;p&gt;The ThemeIsle SDK ships inside dozens of plugins as a composer dependency. Its &lt;code&gt;Featured_plugins.php&lt;/code&gt; module did something I found more interesting than a deletion, in the version I pulled in late July (SDK 3.3.52):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$featured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query_plugins_by_author&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$plugins&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$featured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$plugins&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$plugins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'results'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$plugins&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge your own plugins to the front of the Featured tab, then slice the list back to its original length. The count never changes, so nothing looks wrong from the outside - but for every plugin inserted at the top, one genuine wp.org featured plugin drops off the bottom. Up to three, silently. The inserted slugs were fixed in the code: &lt;code&gt;optimole-wp&lt;/code&gt;, &lt;code&gt;otter-blocks&lt;/code&gt;, &lt;code&gt;wp-cloudflare-page-cache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things in ThemeIsle's favour, because they're true. The module bails unless the current user can install plugins, so it never touches a subscriber or an editor. And &lt;code&gt;themeisle_sdk_disable_featured_plugins&lt;/code&gt; is a real off switch - a code filter rather than a checkbox, so a site owner won't find it, but it exists, and I didn't find an equivalent in anything else I read for this.&lt;/p&gt;

&lt;p&gt;Then the second layer, which was the part you'd never catch from the screen. The module is gated on a self-declared header. &lt;code&gt;Product.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;wordpress_available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'yes'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$file_headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'WordPress Available'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a plugin declares &lt;code&gt;WordPress Available: yes&lt;/code&gt;, the module stays dormant - the SDK treats free wp.org builds as exempt. But if the header is missing entirely, that comparison is false, &lt;code&gt;can_load()&lt;/code&gt; doesn't bail, and the module runs. Seven ThemeIsle plugins - the old Webcraftic / Clearfy ones, acquired and republished under the Themeisle account - never carried that header, so the module ran in their free builds. Roughly 259,000 installs between them, as of 29 July 2026. It read like an acquisition oversight rather than a decision, and that's how I planned to write it. All 11 ThemeIsle themes carrying the SDK declared the header and had the module dormant. I have no Pro builds and no licences, so I claim nothing about what those execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is any of this against the rules?
&lt;/h2&gt;

&lt;p&gt;I couldn't find a guideline that named this behaviour, so I asked the plugin review team in the Make WordPress Slack, in &lt;code&gt;#pluginreview&lt;/code&gt;, whether the handbook should say something about rewriting installer results. A reviewer, frantorres, answered that I had the question backwards:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Regarding this, the guidelines cannot be a collection of every single detail that could be wrong, it would be endless and still never accurate, it is already quite long actually.The guideline 9 says: 9. Developers and their plugins must not do anything illegal, dishonest, or morally offensive.Removing results from the search without telling anyone about it sounds dishonest.The guideline 11: 11. Plugins should not hijack the admin dashboard.Removing elements from the dashboard (when it is not something intended by a feature of the plugin that the user is aware of and which makes sense for that feature) sounds like hijacking the admin dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the rules already cover it. But neither guideline names it. Guideline 9's enumerated examples are all about wordpress.org itself - keyword stuffing, black hat SEO, fake reviews, sockpuppet accounts. Guideline 11's text is about nags, notices and dashboard widgets. Editing the installer results inside a user's own admin appears in neither list, and I think that explains how the pattern spread: a clause broad enough to cover it that nobody ever pointed at it, so everyone shipped. Notice where the emphasis falls in both of the reviewer's readings - on &lt;em&gt;without telling anyone&lt;/em&gt;. The suggestion I made in that thread was small: add this behaviour to the examples under guideline 9, so the clause teaches by example the way it already does for fake reviews.&lt;/p&gt;

&lt;p&gt;That's one reviewer answering in a public channel, and I'm reporting it as that. I also emailed &lt;code&gt;plugins@wordpress.org&lt;/code&gt; with the same question, the list of plugins, and the code. The answer came on 7 August:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Silently removing third-party plugins from the results displayed by the WordPress plugin installer fall under Guideline 9's prohibition on artificially manipulating search results (there are examples, but it covers anything dishonest) and also under guideline 11 of hijacking the admin dashboard (that's not just for notices)For this cases, users can get in touch with the plugin author (for example in the forums) and if they do not respond to that, users can get in touch with us so we can check it.Thank you for reporting a guideline violation in this plugins. We're looking into it right now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both parentheticals are doing the work, and both answer the objection I'd just made. The enumerated examples under guideline 9 aren't the boundary of it - "it covers anything dishonest." And guideline 11 is "not just for notices," which is the reading I'd assumed was unavailable. So the answer to the question in this heading is yes, from the team that enforces it, on both counts.&lt;/p&gt;

&lt;p&gt;The same email says they work case by case, weighing the developer's history, and that responses range from a warning to closing a plugin outright. It also says most reports get no further communication, simply because of the volume, so I wrote that I might never learn how this one ended.&lt;/p&gt;

&lt;p&gt;I found out anyway, and not from them. See below.&lt;/p&gt;

&lt;h2&gt;
  
  
  I told them first
&lt;/h2&gt;

&lt;p&gt;At this point I could have published. Instead I emailed both vendors - eLightUp on 28 July, ThemeIsle the same day - with what I found, the exact code, the questions I had, and the date I planned to publish. I'd rather print a vendor's reason than my guess, and if I'd read something wrong I wanted the chance to fix it before it went out rather than after.&lt;/p&gt;

&lt;h2&gt;
  
  
  One of them fixed it
&lt;/h2&gt;

&lt;p&gt;The ThemeIsle side is best told by the clock.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;28 July, 15:22 GMT - I sent the email.&lt;/li&gt;
&lt;li&gt;29 July, 08:50 GMT - all seven plugins were updated on wordpress.org, carrying SDK 3.3.58.&lt;/li&gt;
&lt;li&gt;29 July, 15:14 GMT - Ionut Neagu's reply arrived, about six hours after the fix was already live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't take the email's word for it. I downloaded the shipped zips from wordpress.org the same day: disable-admin-notices 1.4.7, remove-category-url 1.2.4, cyrlitera 1.3.6, cyr-and-lat 1.3.5, instagram-slider-widget 2.3.5, webcraftic-updates-manager 1.3.3, comments-plus 1.3.3. Every one now declares &lt;code&gt;WordPress Available: yes&lt;/code&gt; in its main file, which means the module no longer runs in any ThemeIsle free build.&lt;/p&gt;

&lt;p&gt;And they fixed more than the header. One of my three questions had been why the list gets sliced back to its original length instead of lengthened - lengthening would insert their plugins without displacing anyone. In 3.3.58 the &lt;code&gt;array_slice&lt;/code&gt; is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$featured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query_plugins_by_author&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$original_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$plugins&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;remove_plugins_by_slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_plugin_slugs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$featured&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$featured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$plugins&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;adjust_results_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$original_count&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list now lengthens, duplicates get removed, and the result count is raised by the difference so pagination stays honest. Nothing falls off the bottom anymore. They'd answered my question in code before answering it in prose. Neagu confirmed the gate reading was correct, called the missing header an oversight from the acquisition integrations, and asked that the corrections make it into the published piece. They have. With the header restored the module doesn't run in the free builds at all, so on wordpress.org there's now nothing on the Featured tab to see. What ships in the Pro builds I can't tell you - I have no licences and haven't looked. The displacement and the accidental free-build loading, the two things I flagged, were both gone within seventeen and a half hours of the email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other one didn't
&lt;/h2&gt;

&lt;p&gt;Anh Tran, founder of eLightUp, replied the same day I wrote to him. His answer, in full and unedited:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What you found is correct. Meta Box and Slim SEO include the same code that removes Secure Custom Fields (SCF) from the Featured and Recommended tabs in the WordPress plugin installer, and there is currently no UI setting or mention of it in our readmes.The reason is straightforward: we believe SCF is an unusual case in the WordPress ecosystem. While WordPress' licensing allows forking, SCF was created by taking the commercial version of Advanced Custom Fields rather than the free version available on wordpress.org. Many people in the community have expressed concerns about whether wordpress.org should be distributing a fork of a premium product under those circumstances.Meta Box operates in the custom fields space, so SCF is naturally a direct competitor to us. However, our decision wasn't based solely on competition. If a third party—not WordPress.org or Automattic—had taken a premium plugin, renamed it, and redistributed it publicly, we believe the community reaction would likely have been very different.We understand that reasonable people can disagree on this topic. Some see it as an acceptable use of the GPL; others see it as crossing an ethical line despite being legally permissible. We fall into the latter camp.We're not attempting to prevent users from installing or using SCF. The code only removes it from the Featured and Recommended lists shown within our plugins. Users can still search for it directly, install it manually, and make their own choice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm not going to rule on the SCF question. Whether wordpress.org should distribute a fork of a commercial plugin is a real disagreement that predates this post, people I respect land on both sides of it, and it isn't what I measured. He answered a courtesy email honestly and openly within hours, and that counts for something.&lt;/p&gt;

&lt;p&gt;The facts as of 29 July: the code ships in meta-box 5.13.1 and slim-seo 4.9.11, roughly 570,000 installs combined, and removes one named competitor from two tabs. There's no setting and no readme line. He keeps it because he treats SCF as an ethical edge case rather than an ordinary competitor, and he said so plainly when I asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then they were told to remove it
&lt;/h2&gt;

&lt;p&gt;On 10 August, Anh Tran emailed again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We've now heard back from the WordPress.org plugins team. They've asked us to remove this behavior from both Meta Box and Slim SEO, and we'll be complying with that request. The changes will be included in our upcoming releases.I thought it was worth letting you know, as this changes the current status. While the broader discussion around disclosure and user choice is still relevant, readers should also know that this behavior is already being removed following guidance from the plugins team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the report I sent on 28 July did land. The plugins team told me on 7 August that they were looking into it, and by 10 August they had asked the vendor to remove it and the vendor had agreed. That is quick, on a thing that had been shipping unremarked for months.&lt;/p&gt;

&lt;p&gt;He asked that readers know the behaviour is already being removed, and it's a fair request, so here it is up front. One qualifier, because "already" is doing a lot of work: it's announced, not shipped. I checked the current releases this morning, on 10 August. &lt;code&gt;src/FeaturedPlugins.php&lt;/code&gt; is still in meta-box 5.14.0 and in slim-seo 4.9.11, and it still unsets &lt;code&gt;secure-custom-fields&lt;/code&gt; on both tabs. Nothing has been removed yet. It's coming in releases that haven't happened.&lt;/p&gt;

&lt;p&gt;Two smaller things changed while I was writing, both in meta-box 5.14.0 on 30 July. The two files are no longer byte for byte identical, and meta-box now wraps the removal in a filter, &lt;code&gt;rwmb_modify_plugin_recommendations&lt;/code&gt;, so another developer can switch it off in code. Slim SEO has no such filter. That is a real change from what I described above, and it's worth being exact about what it is: a hook for developers, not a setting a user can find, and still nothing in either readme.&lt;/p&gt;

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

&lt;p&gt;Same hook, same screen, same email sent the same day with the same notice. One vendor shipped a fix before his reply reached me. The other wrote back within hours, explained why he treats SCF as a case apart, and kept the code - and then agreed to remove it after wordpress.org asked him to. Both engaged from the first email. They took different routes to the same place.&lt;/p&gt;

&lt;p&gt;Worth separating the two things that happened there. eLightUp's reasoning hasn't changed, and nobody has said it was wrong to hold it: the SCF fork question is a real disagreement and it long predates this post. What changed is that the plugins team read guidelines 9 and 11 as covering the behaviour, and that reading settled it. A reviewer in a public channel got there first, reading the same handbook.&lt;/p&gt;

&lt;p&gt;I don't think that gap is an accident. Nothing in either guideline names this behaviour exactly. Guideline 9's examples are all about wordpress.org itself, guideline 11 reads like it's about notices, and a developer checking whether the rules cover editing installer results would have found nothing pointed at it. So the code shipped, in two plugins, for months, until someone asked out loud.&lt;br&gt;
The rule was always there. But it is generic and broad so the wording left room for interpretation ( by both the authors and reviewers ).&lt;/p&gt;

&lt;p&gt;The "Add New" screen still works the way it did when I started. WordPress asks the directory for a list, and everything already installed gets to edit the answer before you see it with a filter. But nearly no-one does.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>plugins</category>
    </item>
    <item>
      <title>July Was Intense</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Wed, 05 Aug 2026 14:28:19 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/july-was-intense-389k</link>
      <guid>https://dev.to/marcindudekdev/july-was-intense-389k</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/july-2026-shipped/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/july-2026-shipped/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;July ran across more than 50 repositories and something like 1,400 commits. Every link below is live as I write this, and I clicked through all of them rather than trusting my own notes.&lt;/p&gt;

&lt;p&gt;I don't usually write these up. This month had enough in it that I wanted the whole thing in one place, if only so I can find it again in a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plugin
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wpmultitool.com/" rel="noopener noreferrer"&gt;WP Multitool&lt;/a&gt; took eight releases in a month, which is the most I've ever shipped of anything in that span.&lt;/p&gt;

&lt;p&gt;Most of that was making the thing feel finished rather than adding to it. It's properly translated now, all the way through, instead of the half-done string files most plugins carry around. The object cache diagnostics got rebuilt so they check a whole spread of things at once and tell you whether the cache is actually doing work, which is a different question from whether it's switched on. The Config Manager can put your wp-config back the way it was, and that sounds dull right up until the afternoon you need it.&lt;/p&gt;

&lt;p&gt;I also taught the release script to refuse. It now runs its own checks before anything ships, and a release that doesn't pass them doesn't go out. I built that after one too many evenings catching my own mistakes by hand, and it has caught things since, which is the whole point.&lt;/p&gt;

&lt;p&gt;Three pages went up around it. There's a &lt;a href="https://wpmultitool.com/agencies/" rel="noopener noreferrer"&gt;landing page for agencies&lt;/a&gt;, because someone running fifty sites has a different problem from someone running one. Everything I'd written about &lt;a href="https://wpmultitool.com/autoload-bloat/" rel="noopener noreferrer"&gt;autoload bloat&lt;/a&gt; got pulled into one place instead of being scattered across half the site. And the &lt;a href="https://wpmultitool.com/changelog/" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; now drives updates through Polar, so people who bought the plugin get new versions the ordinary WordPress way instead of downloading a zip like it's 2013.&lt;/p&gt;

&lt;p&gt;I wired up conversion tracking too, self-hosted through Umami, so I can see what people actually do on the site without handing that to anyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things that tell you what's wrong
&lt;/h2&gt;

&lt;p&gt;Three of my sites exist to point at problems, and all three got better at it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://makewpfast.com/badge/" rel="noopener noreferrer"&gt;speed badge&lt;/a&gt; used to show a letter grade. It now shows how long the server actually took to answer, in real numbers, measured on the homepage. A letter is something you can argue with. A measurement is harder to wave away. I wrote up how the whole thing works &lt;a href="https://makewpfast.com/wordpress-plugin-speed-badge/" rel="noopener noreferrer"&gt;in a post&lt;/a&gt;, and while I was in there I rewrote the rest of &lt;a href="https://makewpfast.com/" rel="noopener noreferrer"&gt;makewpfast.com&lt;/a&gt; into first person. It used to read like a company. There is no company, there's me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://whydoesmysitesuck.com/" rel="noopener noreferrer"&gt;Why Does My Site Suck&lt;/a&gt; got its permissions redesigned and then a long run of hardening passes on the scanner. Each one came out of me sitting down and trying to break my own thing, which is unglamorous work and much better than a user finding it first.&lt;/p&gt;

&lt;p&gt;And &lt;a href="https://fix-wp.com/" rel="noopener noreferrer"&gt;fix-wp.com&lt;/a&gt; went through a complete cleanup in a single day. Security contact details, images that load late without the page jumping around underneath you, fonts served from my own server, caching set up properly, and a tightened policy on what the browser is allowed to load. It came out of that noticeably faster and noticeably tidier. One day, start to finish, which is the first time that particular loop has run that cleanly for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things that sell
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ultrafastwoo.marcindudek.dev/" rel="noopener noreferrer"&gt;UltrafastWoo&lt;/a&gt; turned into an actual product this month. It moved onto Cloudflare's hosting, got its own social card, the files that tell AI crawlers what they're looking at, and three purchase tiers through Polar. Next to it there's &lt;a href="https://litespeed-demo.marcindudek.dev/" rel="noopener noreferrer"&gt;a live demo&lt;/a&gt; you can poke at, because when you're selling speed a working demo beats a paragraph of adjectives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://merkdex.com/" rel="noopener noreferrer"&gt;merkdex&lt;/a&gt; finally started pulling real offers in from outside, which it had never managed before. It also picked up self-serve registration, so people can sign themselves up instead of waiting on me, and it now takes price snapshots every week so there's a history building up rather than just a snapshot of today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fairprice.work/" rel="noopener noreferrer"&gt;fairprice.work&lt;/a&gt; moved onto its own domain, got a feedback widget, and had its homepage picked apart and rebuilt properly. &lt;a href="https://curlship.com/" rel="noopener noreferrer"&gt;curlship&lt;/a&gt; got an endpoint for pulling a single listing, the privacy and terms pages it should have had from the start, a rate limit on the upgrade route, and a pile of small interface fixes. &lt;a href="https://hunazo.com/" rel="noopener noreferrer"&gt;hunazo&lt;/a&gt; got verified testimonials sitting right under the signup button, where they're actually useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things people use
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chat.marcindudek.dev/" rel="noopener noreferrer"&gt;TeleChat&lt;/a&gt; is live chat you answer from Telegram, which means no dashboard to keep open and no app to remember. The landing page and the panel both got redesigned this month, I wrote it a proper deploy script, and it moved onto its own domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kiedy.marcindudek.dev/" rel="noopener noreferrer"&gt;kiedy&lt;/a&gt; is a small scheduling tool, and I moved it onto a different framework, cleaned up the duplicate days it kept producing, and made the dates sort in the order dates go in. The business version now runs on &lt;a href="https://kiedy-biz.marcindudek.dev/" rel="noopener noreferrer"&gt;its own host&lt;/a&gt; instead of sharing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://windma-app.marcindudek.dev/" rel="noopener noreferrer"&gt;windma&lt;/a&gt; got a new name, an AI moderation board, and a landing page pointing at the beta. It also got a fix for a live connection that had been quietly dropping after a few seconds, which explained a lot of behaviour I'd spent weeks blaming on other things.&lt;/p&gt;

&lt;p&gt;Two of them aren't work at all. &lt;a href="https://totaleclipse2026.marcindudek.dev/" rel="noopener noreferrer"&gt;The eclipse site&lt;/a&gt; got a visual overhaul, buttons that drop the event straight into your calendar, and a chunk of it translated into a fair number of languages. And &lt;a href="https://rower.marcindudek.dev/" rel="noopener noreferrer"&gt;rower.marcindudek.dev&lt;/a&gt; is a bike trip, written up day by day with the photos, now sitting on a domain of its own. I enjoyed building that one more than anything else on this list.&lt;/p&gt;

&lt;p&gt;Over on wordpress.org, &lt;a href="https://wordpress.org/plugins/zero-blocks-given/" rel="noopener noreferrer"&gt;Zero Blocks Given&lt;/a&gt; shipped twice and rolled out across the sites I look after.&lt;/p&gt;

&lt;h2&gt;
  
  
  My own site
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/press/" rel="noopener noreferrer"&gt;marcindudek.dev&lt;/a&gt; got a Press and Media page in both languages, carrying its first citation and the "Backend Bottlenecks Exposed" interview I did for WP Plugins A to Z. That felt good to put up.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://marcindudek.dev/offerings/" rel="noopener noreferrer"&gt;offerings page&lt;/a&gt; picked up three new things and a search box in the filter bar, which starts to matter once the list is longer than a screen. I published three posts: &lt;a href="https://marcindudek.dev/blog/one-shot-code-review-benchmark/" rel="noopener noreferrer"&gt;a benchmark of one-shot code review&lt;/a&gt; with the cost per run charted out, &lt;a href="https://marcindudek.dev/blog/consolidate-permission-prompts/" rel="noopener noreferrer"&gt;285 permission rules, only 41 did anything&lt;/a&gt;, and &lt;a href="https://marcindudek.dev/blog/animated-qr-file-transfer/" rel="noopener noreferrer"&gt;a piece on moving files with animated QR codes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Underneath, I pulled every font onto my own server so the site doesn't call out to Google at all, and rebuilt the mobile navigation as a proper drawer after discovering the old one was close to unusable on an actual phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fleet
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://web-data-labs.com/" rel="noopener noreferrer"&gt;web-data-labs.com&lt;/a&gt; and &lt;a href="https://apify.com/cryptosignals" rel="noopener noreferrer"&gt;the scrapers behind it&lt;/a&gt; got a clearing-out month. I retired the ones that had quietly died and were still sitting in my count as though they were inventory, gave the orphaned pages a proper "this is gone" response instead of leaving them to 404 forever, and repaired the Shopify and IMDb ones.&lt;/p&gt;

&lt;p&gt;The fleet brought in $290.68 in July, about the same as May and June. Small, steady, and it runs without me touching it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built for myself
&lt;/h2&gt;

&lt;p&gt;The unglamorous half of the month went into tooling, none of which has a public address, all of which is why the rest of the list was possible.&lt;/p&gt;

&lt;p&gt;The biggest one is a crawler that audits an entire site the way the expensive desktop tools do, walking every page and checking a long list of things, or pointing at one page when that's all you need. I've since run it against my own sites and it found things I'd have sworn were fine. Alongside it I taught my code grader to judge WordPress code by WordPress conventions rather than by rules from a different world, which cut the false alarms down to almost nothing, and I gave the nginx and LiteSpeed optimizers real test suites so I can change them without holding my breath.&lt;/p&gt;

&lt;p&gt;The rest is plumbing for how I work. My task inbox now reaches me on Telegram and I can answer from there, which means a question doesn't sit unread for six hours. The mobile client became a proper phone app. There's a dashboard showing everything running at once, and a messaging layer between working sessions that killed an entire category of dropped work. A monitor watches for anything burning a processor core for hours and tells me, based on what the process is doing rather than what it's called. The alerting itself now gets verified end to end instead of assumed to work. My browser automation grew up enough that every testing pass runs through it. There's a register of which port belongs to what, so two things stop fighting over the same one. And a job checks in on plugin install counts from wordpress.org every day so I can see movement rather than guess at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client side
&lt;/h2&gt;

&lt;p&gt;Alongside my own things there was client work, and most of my hours went there. A sales and service system with payments and notifications running through it. A set of blocks and templates for a large product site. Log hygiene and a platform update for a hosting company. Performance research on an e-commerce engine. An events plugin, a batch of marketplace fixes, and a service site. No names and no numbers on any of it, because that part isn't mine to publish.&lt;/p&gt;

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

&lt;p&gt;Three things went away on purpose. I deleted a pile of auto-generated pages for local businesses, because the content was invented and the plan for getting it in front of people didn't survive contact with the rules I actually have to follow. I closed a marketing channel that had returned nothing at all in four months of feeding it. And I switched off a page-generation pipeline after it ran itself out of memory. All three were mine to kill, all three were the right call, and none of them cost me more than an afternoon to walk away from.&lt;/p&gt;

&lt;h2&gt;
  
  
  August
&lt;/h2&gt;

&lt;p&gt;Fewer new things, more depth on the ones that already have people using them. The plugin has the most momentum, so it gets the most of my time. Everything else on this list stays up and stays maintained.&lt;/p&gt;

&lt;p&gt;If you'd rather have the running version than the monthly one, there's &lt;a href="https://marcindudek.dev/devlog/" rel="noopener noreferrer"&gt;a devlog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>monthlyreview</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Britain Chose Lowercase Road Signs Before Testing Them</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Tue, 04 Aug 2026 09:59:49 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/britain-chose-lowercase-road-signs-before-testing-them-1pn1</link>
      <guid>https://dev.to/marcindudekdev/britain-chose-lowercase-road-signs-before-testing-them-1pn1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/road-signs-lowercase-evidence/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/road-signs-lowercase-evidence/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The short version&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;British road signs use mixed case. The reason everyone gives is that lowercase is faster to read. I went looking for the study behind that claim and found a &lt;strong&gt;chronology problem&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The committee provisionally settled on lowercase in &lt;strong&gt;October 1958&lt;/strong&gt;. The signs went up on the Preston By-Pass that December. The only controlled experiment ran in &lt;strong&gt;1960&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;That experiment put the competing letterforms within &lt;strong&gt;8 feet of each other on a 240-foot reading distance&lt;/strong&gt;. Its own authors wrote that the choice could therefore be made on aesthetic grounds.&lt;/li&gt;
&lt;li&gt;Both official reports then did exactly that. Worboys, 1963: &lt;em&gt;"because we prefer it."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The word-shape theory used to justify lowercase has since been dropped by cognitive psychology. Mixed case does have real advantages, they're just not the ones people cite.&lt;/li&gt;
&lt;li&gt;There's an &lt;a href="https://marcindudek.dev/blog/road-signs-lowercase-evidence/#timeline" rel="noopener noreferrer"&gt;interactive timeline&lt;/a&gt; below - click any entry, every one links to a source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was reading about Margaret Calvert, the designer who gave Britain its road signs, and someone stated the usual thing: the signs use lowercase because &lt;strong&gt;lowercase is read faster than UPPERCASE&lt;/strong&gt;. No study attached. I assumed that was laziness and went to find it myself.&lt;/p&gt;

&lt;p&gt;A day of digging later I had the studies. The problem is what they say, and when they happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim, and why it's slippery
&lt;/h2&gt;

&lt;p&gt;"Lowercase is read faster" sounds like one claim. It's three, and they have completely different evidence behind them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Continuous prose is read faster in lowercase.&lt;/strong&gt; True, well measured, roughly 13%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reason is word shape.&lt;/strong&gt; Dropped by cognitive psychology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Therefore a road sign glimpsed at 70mph is easier in lowercase.&lt;/strong&gt; This is the one that doesn't hold, and it's the one people mean.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claim 1 comes from Miles Tinker and Donald Paterson, 1928. 640 university students, a timed reading test, 1¾ minutes each. Tinker summarised his own result plainly: lowercase type was read &lt;strong&gt;13.4% faster than all capitals&lt;/strong&gt;. A later study on prolonged reading put the range at 9.5% to 19%, which is where the popular "10-20%" figure comes from.&lt;/p&gt;

&lt;p&gt;Solid work. But look at what Tinker himself said caused it, because this part gets dropped every single time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Faster reading of the lower-case print is due to the characteristic word forms furnished by this type. This permits reading by word units, while all capitals tend to be read letter by letter. Furthermore, since all-capital printing takes at least one-third more space than lower case, more fixation pauses are required for reading the same amount of material. Miles Tinker, Legibility of Print, p. 65&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Half his explanation is geometry. All-caps took about 35% more area in his setup, because both versions were set at the same point size and nobody matched the x-height. Bigger text means fewer words per eye fixation. Tinker described the confound and left it in. So the headline number is partly measuring "capitals are physically larger", which is a fact about typesetting, not about the human visual system.&lt;/p&gt;

&lt;p&gt;That matters enormously for a road sign, where the whole design problem is how much you can fit on a panel of a given size.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dates don't work
&lt;/h2&gt;

&lt;p&gt;Here's where it stopped being a typography question for me and became a records question. Ole Lund, a researcher at Reading, went through the committee papers and the National Archives files, and the sequence he lays out is this.&lt;/p&gt;

&lt;p&gt;Filter&lt;/p&gt;

&lt;p&gt;No events in that category.&lt;/p&gt;

&lt;p&gt;Read the middle of that in order. Provisional recommendation October 1958. Signs on a live motorway December 1958. Public row starts March 1959. Experiment 1960. Report 1961.&lt;/p&gt;

&lt;p&gt;The experiment that supposedly justified the decision happened about two years after the decision, and only after a Cambridge University Printer wrote to &lt;em&gt;The Times&lt;/em&gt; to complain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the experiment found
&lt;/h2&gt;

&lt;p&gt;The Road Research Laboratory ran it properly for 1960. Mock road signs mounted on a car, driven toward seated RAF volunteers at Benson airfield in Oxfordshire, thousands of recorded reading distances. Four letterforms. Here are the means:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Letterform&lt;/th&gt;
&lt;th&gt;Mean reading distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kindersley, seriffed capitals&lt;/td&gt;
&lt;td&gt;247 ft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinneir, lower case&lt;/td&gt;
&lt;td&gt;240 ft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Johnston-based capitals&lt;/td&gt;
&lt;td&gt;239 ft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinneir, smaller, generous margins&lt;/td&gt;
&lt;td&gt;212 ft&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight feet separates first from third. On 240. Christie and Rutley, who ran it, didn't oversell what they had:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The most remarkable feature of the results for the three […] scripts is that the reading distances are so nearly equal […] the difference is so small that caution is necessary in interpreting its meaning. […] Since there is little difference in legibility between the different types of lettering, it seems reasonable to make the choice on aesthetic grounds. Christie &amp;amp; Rutley, Road Research Laboratory, 1961&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They went further and suggested that aesthetic questions were probably at the root of the whole controversy. Which, having read the letters pages, is hard to argue with.&lt;/p&gt;

&lt;p&gt;One more wrinkle. The seriffed capitals that technically came first were at least 20% taller than the x-height of the lowercase they beat. Reading distance scales with letter size. Increase the x-height and the ranking flips. Lund makes this point and the paper's own authors flagged spacing and width-to-height ratio as possible confounds. So even the 8-foot difference isn't clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the committees wrote down
&lt;/h2&gt;

&lt;p&gt;This is my favourite part, because it's not interpretation. It's the official reports saying it themselves.&lt;/p&gt;

&lt;p&gt;The Anderson report, 1962, paragraph 17, brings up the American evidence for lowercase and then immediately undercuts it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;…there is expert American evidence that on signs of comparable size a greater degree of legibility can be achieved by the use of lower rather than upper case lettering. This evidence, however, relates to signs on which only one name appears; tests which our own Road Research Laboratory have carried out on signs with more than one name suggest that where margins and vertical spacing are reduced to the bare minimum… this advantage does not apply. Ministry of Transport, Anderson report, 1962, ¶17&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then paragraph 18 gives the actual reason:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our choice has therefore been finally determined by a wider field of considerations. We have a preference for lower case lettering, which we feel accords better than upper case with the general design of the motorways… We see no point in ourselves pursuing further experiments when there is so little choice between alternative areas and legibilities, and while taste plays so important a part, as we believe it should. Ministry of Transport, Anderson report, 1962, ¶18&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And Worboys in 1963, deciding it for &lt;em&gt;all&lt;/em&gt; British road signs and not just motorways, is even blunter:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We recommend the use of lower-case lettering with initial capitals for traffic signs generally because we prefer it and because we think its outlines are more familiar to the reading eye. Ministry of Transport, Worboys report, 1963, ¶42&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Lund's own verdict on the sequence is careful, and I think correct:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To suggest that the final decision of the Anderson committee was taken already before the Road Research Laboratory performed the tests at Benson airfield in 1960 is perhaps to overstate the issue, but nevertheless, a feeling that the experiments were some kind of play to the gallery - only necessitated by the public debate and performed in order to shrug it off - is hard to avoid. […] It is correct to say that the committee had no plans whatsoever to abandon Kinneir's solution, regardless of the outcome of the Road Research Laboratory's experiment. Ole Lund, Typography Papers 5, p. 118&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The distinction that breaks the whole claim
&lt;/h2&gt;

&lt;p&gt;If you only take one thing from this post, take this one. It's the piece almost nobody mentions, and once you see it you can't unsee it in any sign study.&lt;/p&gt;

&lt;p&gt;There are two different tasks you can measure on a road sign, and they give opposite answers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recognition&lt;/strong&gt; - the observer already knows which word to look for. This is a driver scanning for BIRMINGHAM because that's where they're going.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legibility&lt;/strong&gt; - the observer has to actually read an unknown word off the sign, letter by letter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Garvey, Pietrucha and Meeker ran both in 1997, with 48 drivers aged 65 and over. This is the study that produced Clearview, the American mixed-case highway font, so it's about as pro-lowercase as sign research gets. Their own summary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The mixed-case Clearview characters outperformed the all-uppercase Series D by as much as 14 percent in daytime and 16 percent at night, as long as the mixed-case font subtended an equivalent sign area. If the mixed-case font took up less sign space… there was no difference between mixed-case and all-uppercase characters. Garvey, Pietrucha &amp;amp; Meeker, Transportation Research Record 1605, 1997 - recognition study&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the win everyone quotes. Now the same paper's other experiment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;…there also were no differences between the all-uppercase Series D font and comparably sized mixed-case Clearview fonts… The all-uppercase Series D significantly outperformed the Clearview Condensed font at 100 percent… The results are consistent with earlier work by Forbes et al., who also found significant improvements with mixed case in a recognition task but not in a legibility task. Same paper - legibility study&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same drivers, same fonts, same day. When they knew the word, mixed case won by 14-16%. When they didn't, it won by nothing. Their legibility index collapsed from about 75 feet per inch of letter height down to about 40. Subjects were nearly twice as good at recognising expected words as at reading unknown ones.&lt;/p&gt;

&lt;p&gt;So the mixed-case advantage is largely a measurement of familiarity. It's the reader's memory doing the work, not the letterforms. Which, if you think about it, is exactly what Tinker's practice-effect explanation predicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  America ran the same movie, and cancelled it
&lt;/h2&gt;

&lt;p&gt;Clearview got provisional US approval in 2004, sold substantially on the mixed-case argument. In January 2016 the Federal Highway Administration terminated that approval. The reasoning reads like a post-mortem of this entire topic:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;…changing the type of retroreflective sheeting alone resulted in a 6% improvement… the apparent improvement of the provisional letter style could be "partly attributed to [its] increased size." Because of the narrowly focused research statement, which examined the cumulative effect of a change to two variables… FHWA technical brief&lt;/p&gt;

&lt;p&gt;…the excessively long legibility distances reported in some of the earlier work were actually the result of recognition, rather than legibility, due to learning effects by the participants among the set of test words. FHWA, same brief&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the line that sums up sixty years of this argument: &lt;em&gt;"In actuality, there is no interdependency between letter style and case."&lt;/em&gt; FHWA's conclusion across all the evaluations was that &lt;strong&gt;the brightness of the retroreflective sheeting is the primary factor in nighttime legibility&lt;/strong&gt;. The font was mostly noise.&lt;/p&gt;

&lt;p&gt;Clearview came back in 2018. Not because of new evidence, because Congress put it in an appropriations bill. FHWA's own memo says so with visible reluctance: &lt;em&gt;"Notwithstanding the reinstatement of this Interim Approval, FHWA makes no recommendation or endorsement of the Clearview letter style."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The theory underneath is dead
&lt;/h2&gt;

&lt;p&gt;The mechanism people give for all of this is word shape, sometimes called &lt;em&gt;bouma&lt;/em&gt;. Lowercase words have ascenders and descenders, so each word gets a distinctive silhouette. Capitals turn every word into a rectangle. Calvert has described her design reasoning in roughly these terms, and every article about the signs repeats it.&lt;/p&gt;

&lt;p&gt;Cognitive psychology moved off that model decades ago, in favour of parallel letter recognition - you identify the letters simultaneously and the word falls out of that. Kevin Larson, who works on reading at Microsoft, addresses the all-caps evidence directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The weakest evidence in support of word shape is that lowercase text is read faster than uppercase text. This is entirely a practice effect. Most readers spend the bulk of their time reading lowercase text and are therefore more proficient at it. When readers are forced to read large quantities of uppercase text, their reading speed will eventually increase to the rate of lowercase text. […] Word shape is no longer a viable model of word recognition. Kevin Larson, The Science of Word Recognition&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a neat corroboration of this hiding in an old ergonomics paper. Foster and Bruce, 1982, found &lt;strong&gt;no difference at all&lt;/strong&gt; in reading speed between uppercase and lowercase when the passages were nonsense strings. The advantage shows up with real words and vanishes with fake ones. That's the signature of familiarity, not of shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest counter-evidence
&lt;/h2&gt;

&lt;p&gt;I'd be doing the same thing I'm complaining about if I only listed the studies that fit. There's real research where &lt;strong&gt;capitals win&lt;/strong&gt;, and some of it is recent and well designed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Arditi &amp;amp; Cho, 2007&lt;/strong&gt;, &lt;em&gt;Vision Research&lt;/em&gt;. Tested normally sighted and low-vision readers with one unaltered font. "Lower-case thresholds were roughly 0.1 log unit higher than upper. Reading speeds were higher for upper- than for mixed-case text at sizes twice acuity size… Results suggest that &lt;strong&gt;upper-case is more legible&lt;/strong&gt; than the other case styles, &lt;strong&gt;especially for visually-impaired readers&lt;/strong&gt;, because smaller letter sizes can be used." That's the opposite of the accessibility advice you usually hear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vartabedian, 1971&lt;/strong&gt;, &lt;em&gt;Human Factors&lt;/em&gt;. With size controlled, uppercase words were &lt;strong&gt;searched 13% faster&lt;/strong&gt; than lowercase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holick et al., 2006&lt;/strong&gt;, for FHWA/Texas. On negative-contrast signs (dark legend on light background), mixed case gave no significant readability gain over the same sign in all-uppercase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put all of it together and the picture is genuinely mixed, which is roughly what &lt;a href="https://www.roads.org.uk/articles/war-worboys/anderson-and-kindersley" rel="noopener noreferrer"&gt;Roads.org.uk concluded&lt;/a&gt; too: the evidence on uppercase versus mixed case on signs "is rather mixed to this day."&lt;/p&gt;

&lt;h2&gt;
  
  
  Two stories with no source at all
&lt;/h2&gt;

&lt;p&gt;While chasing citations I lost two things I'd assumed were solid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60mph in the rain at night story.&lt;/strong&gt; You'll find it everywhere: Kinneir and Calvert testing prototypes by driving past them at 60mph, in the rain, at night. I could not trace it to any primary or contemporaneous source. What &lt;em&gt;is&lt;/em&gt; documented, via Lund, is much more modest: &lt;em&gt;"informal 'low-tech' experiments: with reflective material in an underground garage in order to determine a sensible weight; and in Hyde Park in London in order to determine sensible appearance-widths and a sensible x-height."&lt;/em&gt; No recorded numbers from either. There were also demonstrations for the committee on the runways at Hendon in August 1958, which were demonstrations rather than experiments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dyslexia figure.&lt;/strong&gt; A lot of accessibility writing says all-caps is "13-18% harder to read", often attached to dyslexic readers specifically. I went after this one hard. The British Dyslexia Association's Style Guide 2023 is where it usually gets attributed, so I read it. Its entire capitalisation content is one sentence - &lt;em&gt;"Avoid using capital letter and uppercase letters for continuous text. Lower case letters are easier to read."&lt;/em&gt; - and the document has no bibliography at all. The 13-18% number floats around university style guides and accessibility posts hedged as "reputed to be", occasionally credited to GOV.UK, which doesn't contain it either. I found no primary source. That doesn't prove one doesn't exist, but after searching the dyslexia reading literature I'd treat it as folklore until somebody produces a paper.&lt;/p&gt;

&lt;p&gt;Worth saying: a claim having no source doesn't make the underlying advice wrong. Long blocks of capitals really are worse to read, and the practical guidance is fine. The number attached to it is made up somewhere along the chain, and made-up numbers are how good advice becomes unfalsifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  So was it a bad decision?
&lt;/h2&gt;

&lt;p&gt;No. That's the part I want to be careful about, because "no evidence" is an easy thing to shout and it isn't what I found.&lt;/p&gt;

&lt;p&gt;Tests happened. Kinneir and Calvert ran informal ones, the RRL ran a formal one. The defensible claim is narrower and, I think, more interesting: &lt;strong&gt;the empirical base was small, confounded and non-decisive, the commitment to lowercase preceded it, and the committees said outright they were choosing on taste.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the design holds up for reasons that have nothing to do with reading speed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Geometry.&lt;/strong&gt; At the same x-height, a name in mixed case is meaningfully narrower than the same name in capitals. Smaller panel, less steel, less wind load, long place names fit without abbreviating. Or invert it: at the same panel width you can give lowercase a bigger x-height, and then it genuinely is visible further away. The Anderson committee was dealing with real width constraints on real signs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recognition.&lt;/strong&gt; A driver hunting for a place name they already know is doing the recognition task, and that's the one task where mixed case measurably wins. Forbes measured it in 1950 and Garvey measured it again in 1997.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System coherence.&lt;/strong&gt; Anderson said the lowercase suited the overall design of the motorways. That's an aesthetic judgement, and I don't think it's a lesser one. A sign system that feels considered gets respected and maintained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kinneir and Calvert made a great call. What bugs me is the story we've built on top of it. Sixty-eight years later, the justification in circulation is a mechanism that got retired, propped up by a percentage from a 1928 reading test that never controlled for letter size, applied to a task nobody in that test performed.&lt;/p&gt;

&lt;p&gt;Good decision, fake receipts. I'd rather have the real reasons, because the real reasons are still true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Everything above links out, but here are the primary documents if you want to check my work rather than take it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ole Lund, &lt;a href="https://jockkinneirlibrary.org/assets/media/Lund-Ole.-2003.-The-public-debate-on-Jock-Kinneir%E2%80%99s-road-sign-alphabet.-Typography-Papers.-5.-103%E2%80%93126.pdf" rel="noopener noreferrer"&gt;"The public debate on Jock Kinneir's road sign alphabet"&lt;/a&gt;, &lt;em&gt;Typography Papers&lt;/em&gt; 5, 103-126. The single best account of this, with the archive references.&lt;/li&gt;
&lt;li&gt;Forbes, Moskowitz &amp;amp; Morgan, &lt;a href="https://onlinepubs.trb.org/Onlinepubs/hrbproceedings/30/30-035.pdf" rel="noopener noreferrer"&gt;"A Comparison of Lower Case and Capital Letters for Highway Signs"&lt;/a&gt;, Highway Research Board Proceedings 30, 1950.&lt;/li&gt;
&lt;li&gt;Garvey, Pietrucha &amp;amp; Meeker, &lt;a href="https://luc.devroye.org/Garvey+Pietrucha+Meeker-TransportationResearchRecord-1605.pdf" rel="noopener noreferrer"&gt;"Effects of Font and Capitalization on Legibility of Guide Signs"&lt;/a&gt;, TRR 1605, 1997.&lt;/li&gt;
&lt;li&gt;Miles Tinker, &lt;a href="https://gwern.net/doc/design/typography/1963-tinker-legibilityofprint.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;Legibility of Print&lt;/em&gt;&lt;/a&gt;, 1963. The 13.4% is in his own summary of the 1928 study.&lt;/li&gt;
&lt;li&gt;Kevin Larson, &lt;a href="https://learn.microsoft.com/en-us/typography/develop/word-recognition" rel="noopener noreferrer"&gt;"The Science of Word Recognition"&lt;/a&gt;, Microsoft Typography.&lt;/li&gt;
&lt;li&gt;FHWA, &lt;a href="https://www.federalregister.gov/documents/2016/01/25/2016-01383/termination-of-interim-approval-for-use-of-clearview-font-for-positive-contrast-legends-on-guide" rel="noopener noreferrer"&gt;Termination of Interim Approval for Clearview&lt;/a&gt;, 81 FR 4083, 2016.&lt;/li&gt;
&lt;li&gt;Arbel &amp;amp; Toler, &lt;a href="https://gwern.net/doc/design/typography/2020-arbel-2.pdf" rel="noopener noreferrer"&gt;"ALL-CAPS"&lt;/a&gt;, &lt;em&gt;Journal of Empirical Legal Studies&lt;/em&gt; 17, 2020. Not about signs, but the best modern test of whether capitals help anyone notice anything. They don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Christie and Rutley's 1961 paper isn't online anywhere I could find. I'm quoting it via Lund, who worked from the originals and the unpublished RRL research note. Same for the Anderson and Worboys paragraphs, which I've checked against the report PDFs. I haven't been to the National Archives files at Kew myself, so anything sourced to MT 113/46 or MT 126/1 here is secondhand through Lund's footnotes.&lt;/p&gt;

&lt;p&gt;If you've got the Christie and Rutley paper, or you can produce a real citation for that 13-18% dyslexia figure, I'd like to see it. I did the same kind of chase on &lt;a href="https://marcindudek.dev/blog/plastic-eating-bacteria/" rel="noopener noreferrer"&gt;plastic-eating bacteria&lt;/a&gt; a while back and got a couple of corrections that improved the post.&lt;/p&gt;

&lt;p&gt;Written August 4, 2026. Every claim links to a primary document - committee reports, the original studies, or the Federal Register. I'm a developer, not a typographer or a historian. If I've misread a source, tell me and I'll fix it.&lt;/p&gt;

</description>
      <category>design</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Animated QR file transfer: good optical modem, bad file-transfer product</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:33:39 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/animated-qr-file-transfer-good-optical-modem-bad-file-transfer-product-332f</link>
      <guid>https://dev.to/marcindudekdev/animated-qr-file-transfer-good-optical-modem-bad-file-transfer-product-332f</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/animated-qr-file-transfer/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/animated-qr-file-transfer/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstract.&lt;/strong&gt; &lt;a href="https://github.com/deedy/qr-data-transfer" rel="noopener noreferrer"&gt;deedy/qr-data-transfer&lt;/a&gt; streams a file as animated QR codes on your screen and reads them back with a phone camera. Three things are true at once. The engineering is good, and the fountain-code choice under it is the right one. The idea is eight years old and already ships in production elsewhere. And as a general file-transfer tool it loses to a USB stick by two to four orders of magnitude. The README's 1.40 Mbps is a PHY rate, payload bytes times frames per second, and it says "before camera loss" right there in the same sentence. What's missing is anyone publishing the other number, the one you actually get. Below: how it works, why the number splits in two, and the narrow set of situations where shining data at a camera is the correct answer.&lt;/p&gt;

&lt;p&gt;This one is trending today, so I sent it to Grok for an independent read and then checked the load-bearing claims myself. Two of them I verified by hand. Every throughput figure in this post is an estimate, mine or Grok's, and I say so again where they appear.&lt;/p&gt;

&lt;p&gt;The short version: it's a well-built optical modem, an old idea, and a bad general-purpose product. All three at once. The verdict is the boring half. The interesting half is why the headline number and the number you get are two different numbers.&lt;/p&gt;

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

&lt;p&gt;The sender runs entirely in the browser. Your file goes through Brotli-q11 or gzip-9, whichever comes out smaller, into a container with metadata and a CRC. That container is chopped into RFC 6330 RaptorQ fountain symbols by a WASM codec. Each symbol becomes a binary QR frame: magic bytes &lt;code&gt;QF4&lt;/code&gt;, a 19-byte header, the payload, a CRC-32. Raw byte mode, not Base45, which buys back real capacity. Those frames animate on screen in one or two lanes, synced to the display refresh.&lt;/p&gt;

&lt;p&gt;The receiver opens &lt;code&gt;/scan&lt;/code&gt; on a phone. Camera into ZXing-C++ compiled to WASM, frames filtered by CRC, RaptorQ decodes, a multi-CRC check runs, file saves. There's a legacy path in the tree with a hand-rolled Luby LT code and Base45 framing, tagged &lt;code&gt;QF3:&lt;/code&gt;, but that isn't the production route anymore.&lt;/p&gt;

&lt;p&gt;The payload never touches the network. You do download the app JavaScript on first use, so "zero install" is half true until it caches.&lt;/p&gt;

&lt;h2&gt;
  
  
  RaptorQ is the right call
&lt;/h2&gt;

&lt;p&gt;This is the part I want to give proper credit for. A screen shining at a camera is a one-way lossy channel with no back-channel. The receiver can't ask for a retransmit, can't ACK, and probably wasn't even pointed at the screen when the stream started.&lt;/p&gt;

&lt;p&gt;A fountain code solves exactly that. RaptorQ generates an effectively unlimited stream of symbols from the source block, and the receiver reconstructs the file once it has collected slightly more than the original amount, no matter which symbols those were. Point your phone at the screen halfway through and it still works. Blink, drop a dozen frames, still works. No sequencing, no windowing, no negotiation.&lt;/p&gt;

&lt;p&gt;Naive frame replay, where you loop frames 1 to N forever and hope the receiver eventually catches each one, is what a lot of amateur attempts do. It's much worse. Miss one frame and you wait a full cycle for it to come round again, and the last few frames take absurdly long by coupon-collector logic. RaptorQ is the correct tool here and it's applied correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dual-lane trick is the clever bit
&lt;/h2&gt;

&lt;p&gt;Phone cameras use rolling shutter. The sensor reads out row by row rather than all at once, so if the screen changes mid-readout you get a frame that's half the old QR code and half the new one. Torn frames decode to nothing.&lt;/p&gt;

&lt;p&gt;The usual response is to slow the animation down, which costs you throughput directly. This repo does something better: it puts two QR codes side by side and alternates which one updates. One lane holds still while the other flips. Whatever the camera catches mid-tear, at least one lane in that exposure was stable for the whole readout.&lt;/p&gt;

&lt;p&gt;Two codes at half the flip rate each beat one code at the full rate, because a torn frame is worth zero and a stable frame is worth its full payload. That's a good piece of engineering, and it's the thing I'd steal from this repo.&lt;/p&gt;

&lt;p&gt;It reduces tearing. It doesn't abolish it. Autofocus hunting, glare, moire against the display's pixel grid and hand shake all still cost you frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;The README quotes a nominal 1.40 Mbps optical payload channel "before camera loss". That figure is payload bytes per frame, times frames per second, times eight.&lt;/p&gt;

&lt;p&gt;It's a PHY rate. It's the same class of number as the 1200 Mbps on a Wi-Fi router box, or the 10 Gbps on an SFP module. It describes what the transmitter emits, not what arrives.&lt;/p&gt;

&lt;p&gt;This is where I disagreed with Grok's read. Grok scored the honesty of the claims at 3 out of 10. I think that's harsher than the text supports. The words "before camera loss" are the author's own, sitting in the same sentence as the megabit figure. The author hedged it. Calling that dishonest is unfair.&lt;/p&gt;

&lt;p&gt;The real gap is different. Camera decode rate is not camera frame rate. A phone that records 30 fps will decode considerably fewer dense V30 or V40 codes per second, and the difference depends on lighting, distance, focus, screen brightness and how steady your hand is. That decode rate is the number that matters, and there is no published end-to-end benchmark anywhere in the repo. No wall-clock KB/s for a real file on a real phone. So the only figure a reader gets is the optimistic one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'd actually get
&lt;/h2&gt;

&lt;p&gt;Nobody has measured this repo end to end, including me. The two rows below for the QR path are &lt;strong&gt;estimates&lt;/strong&gt;, and the estimate rests on a guessed 30 to 70 percent decode-loss range. Treat them as an order of magnitude, not a measurement.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;10 MB takes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;USB stick or SD card&lt;/td&gt;
&lt;td&gt;10-100+ MB/s (measured, everywhere)&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AirDrop, LocalSend, python -m http.server&lt;/td&gt;
&lt;td&gt;Tens of MB/s (measured, everywhere)&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated QR, hand-held&lt;/td&gt;
&lt;td&gt;5-40 KB/s (estimated)&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated QR, phone propped up and close&lt;/td&gt;
&lt;td&gt;50-150 KB/s (estimated)&lt;/td&gt;
&lt;td&gt;1-3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The QR rows are inferred from the nominal channel rate minus an assumed decode loss. They are not benchmarks. The upstream codec project claims 184-254 KB/s with four lanes at 30 fps on an iPhone 16, which is its own claim and I did not verify it either.&lt;/p&gt;

&lt;p&gt;The upper row is a lab configuration: phone landscape, propped, close, sender fullscreen, V40 codes, good light. The lower row is what a person does, which is hold a phone in one hand and get bored. Minutes of holding still to move a file is a bad experience, and people bail and email it to themselves instead.&lt;/p&gt;

&lt;p&gt;One detail that reads like a footgun: the browser build accepts files up to 512 MB. At the optimistic end of my estimates that's over an hour of staring at a screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a new idea
&lt;/h2&gt;

&lt;p&gt;This is the claim I checked most carefully, because it's the one that changes how you read the repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/divan/txqr" rel="noopener noreferrer"&gt;divan/txqr&lt;/a&gt; did animated QR plus fountain codes in 2018. LT codes rather than RaptorQ, and Grok cites roughly 25 kbps peak on a 13 KB file. Same architecture, same reasoning, eight years earlier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.blockchaincommons.com/ur/" rel="noopener noreferrer"&gt;Blockchain Commons' Uniform Resources&lt;/a&gt; have been shipping multipart fountain-coded QR in production for years, and if you've moved a PSBT between a hardware wallet and a host you have used one. The &lt;a href="https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md" rel="noopener noreferrer"&gt;BCR-2020-005 spec&lt;/a&gt; is public. That corner of the industry settled on animated fountain QR because it was the correct answer to a real constraint, and it settled a while ago.&lt;/p&gt;

&lt;p&gt;There are several browser clones from 2024 to 2026 too: qifi-dev/qrs, qr-stream, bitfountain. Same genus.&lt;/p&gt;

&lt;p&gt;And this repo depends on someone else's codec. I checked &lt;code&gt;package.json&lt;/code&gt; myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"@raptorqr/core"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0.1.1"&lt;/span&gt;
&lt;span class="s2"&gt;"@raptorqr/fast-qr-wasm"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0.1.1"&lt;/span&gt;
&lt;span class="s2"&gt;"@raptorqr/raptorq-wasm"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0.1.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three come from &lt;a href="https://github.com/infrost/RaptorQR" rel="noopener noreferrer"&gt;infrost/RaptorQR&lt;/a&gt;, a separate project. The codec, the QR encoder and the RaptorQ implementation are upstream. What this repo adds is the &lt;code&gt;QF4&lt;/code&gt; framing, the dual-lane scheduling and a Next 16 front end. That's real work. It isn't a new codec, and the trending post doesn't make that especially clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where shining data at a camera is the right answer
&lt;/h2&gt;

&lt;p&gt;There is a real set of situations here, and it's narrower than "file transfer".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;True air-gapped machines&lt;/strong&gt; where removable media is banned outright, because a USB stick is a malware vector and a screen isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitored or hostile networks&lt;/strong&gt; where the thing you're avoiding is opening a socket at all. No packet, no log line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shared layer 2&lt;/strong&gt;. Guest wifi with client isolation, a corporate laptop next to a personal phone, a captive portal. AirDrop and LocalSend both need a path that doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VDI, Citrix, browser-only kiosks&lt;/strong&gt; where clipboard and USB redirection are locked down and the pixels on screen are the only channel left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small secrets&lt;/strong&gt;. Seed phrases, SSH and GPG keys, PSBT-sized blobs. Hundreds of bytes to low tens of KB, which is seconds even at the pessimistic rate. Hardware wallets landed on this years ago and they were right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what those have in common. In every one of them the other channels are closed, so the value is that this one exists at all. Speed never enters into it. Once AirDrop, scp or a USB port is permitted, this loses on every axis.&lt;/p&gt;

&lt;p&gt;One thing worth saying out loud, because the air-gap framing invites the wrong assumption: this is not confidential. Anybody who can see or film the screen gets the file. The confidentiality is up to you. Encrypt before you transmit if it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I verified and what I didn't
&lt;/h2&gt;

&lt;p&gt;I fetched the README and &lt;code&gt;package.json&lt;/code&gt; before reading Grok's output, so those two are mine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Built on the three @raptorqr/* packages at 0.1.1&lt;/td&gt;
&lt;td&gt;Verified in package.json&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RaptorQ, CRC-32, raw byte mode, Brotli/gzip pick-smaller&lt;/td&gt;
&lt;td&gt;Verified, stated in the README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.40 Mbps is nominal, hedged with "before camera loss"&lt;/td&gt;
&lt;td&gt;Verified, the author's own wording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No published end-to-end benchmark in the repo&lt;/td&gt;
&lt;td&gt;Verified, no wall-clock KB/s anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every KB/s figure in this post&lt;/td&gt;
&lt;td&gt;Estimated. The decode-loss range is a guess&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RaptorQR's 184-254 KB/s&lt;/td&gt;
&lt;td&gt;Upstream's own claim, not checked by me&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;txqr's ~25 kbps, and the other prior-art attributions&lt;/td&gt;
&lt;td&gt;Grok says it fetched them. I did not re-verify&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;Brilliant as a fountain-coded optical modem demo. Unoriginal as an idea. Impractical as a product. If you want a single number on a dumb-to-brilliant axis, Grok pinned it at 4.5 out of 10 and I'd leave it there.&lt;/p&gt;

&lt;p&gt;What I'd take away is smaller than the repo. Fountain codes over a one-way lossy channel, and the dual-lane trick against rolling shutter, are both worth knowing about for any problem where you can't ACK. That's the reusable part. The file-transfer product wrapped around it is a demo of the technique, and it's fine that it is one.&lt;/p&gt;

&lt;p&gt;If you want to poke at it: &lt;a href="https://github.com/deedy/qr-data-transfer" rel="noopener noreferrer"&gt;deedy/qr-data-transfer&lt;/a&gt;. And if anyone does run a real end-to-end benchmark on a phone, that's the missing number and I'd like to see it.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>analysis</category>
    </item>
    <item>
      <title>A one-shot code-review benchmark: scoring restraint over recall across four Claude models</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Fri, 17 Jul 2026 21:45:50 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/a-one-shot-code-review-benchmark-scoring-restraint-over-recall-across-four-claude-models-1od9</link>
      <guid>https://dev.to/marcindudekdev/a-one-shot-code-review-benchmark-scoring-restraint-over-recall-across-four-claude-models-1od9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/one-shot-code-review-benchmark/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/one-shot-code-review-benchmark/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstract.&lt;/strong&gt; I built a small benchmark to answer one question: is Claude Opus 4.6 actually worse than 4.8, or does it just feel older? It scores the half of code review that most evals ignore, which is restraint - not flagging correct code that looks suspicious. Four models, four task families, one-shot and mechanically scored. The two Opus models came out statistically tied, and they cost the same, so there's no reason to pick one on price. Across a real tier gap, Sonnet 5 matched Opus on almost everything while running fastest and costing a third as much. The surprise was Haiku 4.5: it was both the least accurate model and the slowest, because adaptive thinking makes it burn a huge token budget on simple tasks. Every number here is directional, small-n, one machine. The method is the point.&lt;/p&gt;

&lt;p&gt;This started as a boring question. Can I still run Claude Code on Opus 4.6? It's not in the model picker anymore. Ten minutes of poking gave me the answer - pass the full model ID to &lt;code&gt;--model&lt;/code&gt; and the picker just hides it. But the question underneath wouldn't let go. Is 4.6 actually worse than 4.8, or does it only feel older because it's not the default?&lt;/p&gt;

&lt;p&gt;So I built a benchmark. Then I built a better one. Then I found out my benchmark was lying to me, twice, and I had to fix it before I could trust a single number. By the end I'd tested four models across four task families. The most interesting results weren't about which model won. They were about how easy it is to measure the wrong thing and believe it.&lt;/p&gt;

&lt;p&gt;The whole harness is on GitHub: &lt;a href="https://github.com/MarcinDudekDev/llm-review-bench" rel="noopener noreferrer"&gt;MarcinDudekDev/llm-review-bench&lt;/a&gt;. Mistakes documented in the README, on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The design: score restraint
&lt;/h2&gt;

&lt;p&gt;Most "find the bugs" evals reward finding things. That's a problem. Frontier models are very good at finding things, so good that the eval saturates and tells you nothing. My first task had six planted bugs in a FastAPI snippet. Every model found all six. Ceiling hit. Useless.&lt;/p&gt;

&lt;p&gt;The skill that actually separates a good reviewer from a noisy one is the opposite. Knowing when to shut up. A reviewer who flags every line that looks a bit off isn't thorough, they're exhausting. So every task ships three kinds of code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bugs&lt;/strong&gt; - real defects, credited only when the model names the actual mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traps&lt;/strong&gt; - code that looks wrong but is provably correct on the pinned versions. Flagging a trap costs a point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neutral&lt;/strong&gt; - real but stylistic observations that earn neither credit nor penalty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The traps carry the whole thing. An &lt;code&gt;in_array&lt;/code&gt; "type juggling" bug that PHP 8 quietly killed. An unquoted variable that word-splits in bash but not in zsh. A synchronous generator that "blocks the event loop", except Starlette runs it in a threadpool. Each one is bait that a well-read model reflexively flags. The good models are the ones that don't bite.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Four tasks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v1&lt;/strong&gt; - 6 bugs and 2 traps in one snippet. Easy. It ceilinged. I kept it as a documented failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v2&lt;/strong&gt; - 10 obscure version-specific bugs and 6 traps across Python 3.14, PHP 8, and zsh, plus an exact-output asyncio prediction. Hard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3&lt;/strong&gt; - 10 shell-command tasks, scored by running the command and byte-matching stdout. No judge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v4&lt;/strong&gt; - 18 adversarially-filtered hard command tasks. More on that below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1 and v2 are code review, blind-judged by two other models. v3 and v4 are command generation. The model emits one shell command, the harness runs it in a sandbox and compares stdout to a golden value. Binary, mechanical, no opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Three ways my benchmark lied to me
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 A timeout measures the weather, not the model
&lt;/h3&gt;

&lt;p&gt;My first head-to-head used a 60-second cap. Opus 4.6 timed out with no output. Opus 4.8 finished in 28 seconds. Clean win for 4.8.&lt;/p&gt;

&lt;p&gt;It was noise. When I re-ran uncapped, the same model on the same prompt swung from 27 seconds to 132 seconds and back. Cost swung 8x on the same model, which is prompt-cache state, not economics. My "4.8 is faster" result was an artifact of an arbitrary timeout landing on the wrong side of a noisy distribution.&lt;/p&gt;

&lt;p&gt;A tight timeout doesn't tell you which model is faster. It tells you what the API weather was that afternoon. And it hands you a confident, reproducible-looking, completely wrong answer. I nearly shipped it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 "Make it hard" doesn't make it hard
&lt;/h3&gt;

&lt;p&gt;To separate two frontier models I needed genuinely hard tasks. My first instinct was to prompt a strong model to design a brutal one. That produced a 10/10 ceiling twice. Frontier models are bad at knowing what's hard for other frontier models.&lt;/p&gt;

&lt;p&gt;So I used them as difficulty oracles instead. Fable and Grok each proposed candidate tasks. Every reference command was validated by execution. Then each model attempted the other's tasks one-shot, and I kept only the tasks the opposing model got wrong. Those are empirically hard, by construction.&lt;/p&gt;

&lt;p&gt;The result was its own finding. 17 of 18 candidates got solved. Two capable models could not reliably write a shell task that stumps a frontier peer. The single survivor turned on a platform-specific &lt;code&gt;%.2f&lt;/code&gt; rounding quirk that you can't know without running it on that exact host. One-shot BSD shell-command generation is basically saturated for frontier models.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 I wasn't testing what I claimed
&lt;/h3&gt;

&lt;p&gt;Then a simple question. "Are you sure they only get one shot?"&lt;/p&gt;

&lt;p&gt;I wasn't. Claude Code runs with tools enabled by default. Nothing stopped a model from creating the input files, running its command, checking the output, and revising, all before it "answered". The tasks even describe the exact input files. My "one shot, no execution" claim was a hope, not a guarantee.&lt;/p&gt;

&lt;p&gt;I made it structural. Disable tools so execution is impossible, and assert that the model answered in a single turn (a tool call forces a second turn, which now raises instead of scoring):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="s2"&gt;"--output-format"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"num_turns"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# a tool call would force turn 2&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="nf"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"not one-shot"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did it change the result? I guessed it would. I thought tools had been propping up the accuracy ceiling. I was wrong again. Under the guaranteed condition both Opus models were still essentially perfect. The ceiling was real. But now the "one shot" label is true, which matters more than whether the number moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Results
&lt;/h2&gt;

&lt;p&gt;All four models, all four tasks, under the enforced one-shot condition. Review tasks are on a 0-10 judged scale (both judges agreed). Command tasks are exec-scored totals — the denominator is tasks × trials, so on v3 the Opus pair ran 5 trials each (50) and Sonnet/Haiku ran 10 (100); v4 ran 10 trials for all four (180).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Opus 4.6&lt;/th&gt;
&lt;th&gt;Opus 4.8&lt;/th&gt;
&lt;th&gt;Sonnet 5&lt;/th&gt;
&lt;th&gt;Haiku 4.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1 easy review&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2 hard review&lt;/td&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;9.4&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3 short commands&lt;/td&gt;
&lt;td&gt;50/50&lt;/td&gt;
&lt;td&gt;50/50&lt;/td&gt;
&lt;td&gt;100/100&lt;/td&gt;
&lt;td&gt;93/100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v4 hard commands&lt;/td&gt;
&lt;td&gt;180/180&lt;/td&gt;
&lt;td&gt;179/180&lt;/td&gt;
&lt;td&gt;179/180&lt;/td&gt;
&lt;td&gt;122/180&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fh7808tj634rpv7hd1u9o.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%2Fh7808tj634rpv7hd1u9o.png" alt="Grouped bar chart of accuracy per task for Opus 4.6, Opus 4.8, Sonnet 5, and Haiku 4.5. The three larger models cluster near the top on every task; Haiku 4.5 sits well below on v1 (55%) and v4 (68%)." width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Accuracy by task. The three bigger models cluster near the top. Haiku 4.5 drops on the two hard tasks.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Opus 4.6 vs 4.8 is a tie, and they cost the same
&lt;/h3&gt;

&lt;p&gt;On review, difficulty picks the model. 4.6 is a bit better on easy review, 4.8 is clearly better on hard review. On command generation they're indistinguishable. Averaged out, it's a statistical tie.&lt;/p&gt;

&lt;p&gt;And here's the part that answers my original question for anyone hoping 4.6 is the cheap option. It isn't. Anthropic priced the whole Opus 4.x line the same, at 5 dollars per million input tokens and 25 per million output, both with a 1M context window. There's no economic reason to stay on 4.6. Pick on behavior, not price.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Sonnet 5 is the value story
&lt;/h3&gt;

&lt;p&gt;The Opus tie made me want a real tier gap, so I ran everything on Sonnet 5 versus Haiku 4.5. Sonnet 5 is the standout. It matched Opus 4.8 on the hard command set (179/180), stayed within a point on hard review, and it did that while running the fastest of all four models. At 3 dollars per million input tokens against Opus's 5, it's roughly 60% of Opus's per-token price — and cheaper still per run, because it also burns fewer tokens. If I were choosing a default model for review and command work today, on this data I'd reach for Sonnet 5 first.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Haiku 4.5 is where the gap finally shows
&lt;/h3&gt;

&lt;p&gt;Haiku is the one place the benchmark separated models cleanly on accuracy. On the hard command set it scored 122/180, against 179 and 180 for the bigger models. On easy review it managed 5.5 out of 10. The traps and the hard version-specific bugs are exactly where a smaller model gets caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The surprise: Haiku is the slow one
&lt;/h2&gt;

&lt;p&gt;I expected Haiku to be less accurate. I did not expect it to be slow. On the short-command task, median latency:&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;v3 median latency&lt;/th&gt;
&lt;th&gt;v3 median cost/run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;12.8 s&lt;/td&gt;
&lt;td&gt;$0.0247&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;td&gt;80.9 s&lt;/td&gt;
&lt;td&gt;$0.0455&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that twice. Haiku, the model whose entire brand is fast and cheap, was six times slower than Sonnet 5 and cost more per run, on the same tasks. It's priced at a third of Sonnet's per-token rate, yet it cost more, which means it generated far more tokens.&lt;/p&gt;

&lt;p&gt;The cause is adaptive thinking. Haiku 4.5 burns an enormous thinking budget on these problems, which makes it slow and cancels its price advantage at the same time. On this workload the fast, cheap model was neither.&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%2Fj7vs83xpcyomjnm97e28.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%2Fj7vs83xpcyomjnm97e28.png" alt="Grouped bar chart of median latency per task. Haiku 4.5 is the slowest model on v1, v3, and v4, often by a wide margin, while Sonnet 5 is consistently the fastest." width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Median latency by task. Haiku 4.5 is the slowest model on three of four tasks. Sonnet 5 is consistently fastest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you reach for Haiku to save latency and money on anything that needs real reasoning, measure it first. You might be paying more for less.&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%2Fd1tfxqxzhwcml00oau20.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%2Fd1tfxqxzhwcml00oau20.png" alt="Grouped bar chart of median cost per run in USD per task for Opus 4.6, Opus 4.8, Sonnet 5, and Haiku 4.5. The two Opus models are most expensive on every task; on v3 short commands Haiku 4.5 ($0.045) costs more than Sonnet 5 ($0.025) despite a lower per-token price, while on the other three tasks Haiku is the cheapest." width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Median cost per run by task. The two Opus models cost the most everywhere. The Haiku surprise is task-specific: on v3 short commands it costs more than Sonnet 5 ($0.045 vs $0.025) because its thinking budget outweighs its lower per-token price — even though on the other three tasks Haiku is the cheapest model.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Limitations
&lt;/h2&gt;

&lt;p&gt;I want to be honest about what this is. Every number is directional. Small n, one machine, one afternoon of API weather. The review accuracy rests on LLM judges scoring against a rubric another LLM wrote, so I mitigate with two judges from different labs and blind shuffling, and I don't pretend to eliminate the bias. The review scores here are single-trial per model. The command tasks are mechanically scored, which I trust more, but they saturate for frontier models, so they discriminate on precision rather than raw capability. Don't read a 2-point review gap as a law of nature. Read it as a signal worth a bigger run.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What I'd keep
&lt;/h2&gt;

&lt;p&gt;The method held up better than any single result, so that's what I'd carry forward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Score restraint. Once recall saturates, the traps are what separate models.&lt;/li&gt;
&lt;li&gt;Don't report a speed winner at small n. A timeout measures the weather.&lt;/li&gt;
&lt;li&gt;Enforce your constraints in code. "One shot" has to be disabled tools plus a turn-count check, not a line in the prompt.&lt;/li&gt;
&lt;li&gt;Use strong models as difficulty oracles, and accept that they mostly can't stump each other.&lt;/li&gt;
&lt;li&gt;"Fast and cheap" is a claim about a workload, not a model. Haiku's thinking budget made it the slowest here.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next I want to re-run on a quiet machine to strip out the API-weather noise, and judge the review tasks across every trial instead of only the first. That's the real soft spot: the command tasks were mechanically scored over repeated trials, but the review scores still rest on a single judged trial. The harness is built for it now. If you want to poke at it or add your own model, it's all on &lt;a href="https://github.com/MarcinDudekDev/llm-review-bench" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aillm</category>
      <category>benchmarks</category>
    </item>
    <item>
      <title>I Had 285 Claude Code Permission Rules. Only 41 Did Anything.</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Thu, 16 Jul 2026 17:15:57 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/i-had-285-claude-code-permission-rules-only-41-did-anything-5e4e</link>
      <guid>https://dev.to/marcindudekdev/i-had-285-claude-code-permission-rules-only-41-did-anything-5e4e</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/consolidate-permission-prompts/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/consolidate-permission-prompts/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TL;DR&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every time you click &lt;strong&gt;"allow always"&lt;/strong&gt; in Claude Code, a rule gets appended to &lt;code&gt;permissions.allow&lt;/code&gt;. It never gets cleaned up.&lt;/li&gt;
&lt;li&gt;Mine had grown to &lt;strong&gt;285 rules&lt;/strong&gt;. When I actually looked, &lt;strong&gt;only 41 mattered&lt;/strong&gt; - roughly six out of every seven were dead weight.&lt;/li&gt;
&lt;li&gt;The worst offender: &lt;strong&gt;150+ per-domain &lt;code&gt;WebFetch&lt;/code&gt; rules&lt;/strong&gt;, one per site I'd ever fetched, all made pointless the moment a single &lt;code&gt;WebFetch(*)&lt;/code&gt; exists.&lt;/li&gt;
&lt;li&gt;I wrote a skill, &lt;a href="https://github.com/MarcinDudekDev/consolidate-permission-prompts" rel="noopener noreferrer"&gt;consolidate-permission-prompts&lt;/a&gt;, that scans the allowlist, marks every rule already covered by a broader one, and removes the junk - without ever narrowing what's allowed.&lt;/li&gt;
&lt;li&gt;It's the sibling of the built-in &lt;code&gt;/fewer-permission-prompts&lt;/code&gt;. That one stops future prompts. This one clears the mess the past ones left behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I clicked "allow always" a lot. Two years of it. Every new site Claude Code fetched, every git subcommand, every one-off script - "yes, don't ask again." That's the right instinct. Approving things once so you're not nagged forever is the whole point of the allowlist.&lt;/p&gt;

&lt;p&gt;What nobody tells you is that the list only ever grows. There's no janitor. Two years in, I opened my &lt;code&gt;settings.local.json&lt;/code&gt; out of curiosity and counted &lt;strong&gt;285 rules&lt;/strong&gt; in &lt;code&gt;permissions.allow&lt;/code&gt;. That's when I got curious about how many of them were actually doing something.&lt;/p&gt;

&lt;p&gt;The answer was 41. The other 244 were dead weight - rules that could vanish tomorrow and nothing I do would change. Here's how a list rots like that, and the skill I wrote to clean it up safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 150 WebFetch rules that stopped mattering years ago
&lt;/h2&gt;

&lt;p&gt;Here's the clearest example, and probably the one you'll recognize in your own config.&lt;/p&gt;

&lt;p&gt;Every time Claude Code fetches a URL from a domain it hasn't seen, it asks. You approve it, and it writes a per-domain rule. Do that for two years and you get a graveyard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"WebFetch(domain:example.com)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"WebFetch(domain:docs.python.org)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"WebFetch(domain:stackoverflow.com)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"WebFetch(domain:news.ycombinator.com)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"WebFetch(domain:some-blog-i-read-once.dev)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt; &lt;span class="n"&gt;like&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One rule per site I ever fetched. Individually approved, individually stored, all sitting there forever. And then at some point I got tired of approving each new domain and added the broad one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"WebFetch(*)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line allows fetching any URL. Which means the moment it exists, all 150 of the per-domain rules above it are dead weight. Removing every one of them changes nothing about what Claude Code can do, because the wildcard already covers all of them and more.&lt;/p&gt;

&lt;p&gt;A narrow rule sitting under a broad rule that already covers it does nothing. It's just noise you scroll past. Delete all 150 and your permissions are byte-for-byte identical in effect.&lt;/p&gt;

&lt;p&gt;That's the pattern the whole skill is built around: find the rules that are already covered by a broader one, and get rid of them. It's far more common than you'd expect, because "allow always" never checks whether a broader rule already handles the thing you're approving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways the list rots
&lt;/h2&gt;

&lt;p&gt;WebFetch is the vivid one, but the allowlist collects dead weight in three distinct shapes. The skill classifies every rule into one of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Redundant - already covered by something broader
&lt;/h3&gt;

&lt;p&gt;The WebFetch case. Also things like a &lt;code&gt;Bash(git commit:*)&lt;/code&gt; rule when &lt;code&gt;Bash(git *)&lt;/code&gt; already exists somewhere - the specific one is covered by the general one. Removing it is provably safe, because nothing that was allowed stops being allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Junk - one-offs that will never match again
&lt;/h3&gt;

&lt;p&gt;This is the stuff that got captured as a rule but describes a single moment in time. A heredoc commit message you'll never type verbatim again. A &lt;code&gt;mkdir&lt;/code&gt; to one exact absolute path. And my favourite category, shell control-flow fragments that got split into their own rules when a loop was approved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"Bash(done)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"Bash(do ls)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"Bash(while read f)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those will ever be issued as a standalone command again. They're debris. The skill is deliberately conservative here - a real long command that could plausibly re-run stays, and it never touches a deliberate wildcard like &lt;code&gt;Bash(for *)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Collapsible families - narrow rules that could be one wildcard
&lt;/h3&gt;

&lt;p&gt;Sometimes you've approved five subcommands of the same tool separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"Bash(alembic init:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"Bash(alembic revision:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"Bash(alembic upgrade:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"Bash(alembic downgrade:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"Bash(alembic current:*)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those could collapse into a single &lt;code&gt;Bash(alembic *)&lt;/code&gt;. But here's the important difference: that &lt;strong&gt;widens what's allowed&lt;/strong&gt;. It would now permit &lt;code&gt;alembic&lt;/code&gt; subcommands you never approved. So the skill treats consolidation as a separate, explicit opt-in - never bundled with the safe cleanup, and flagged loudly when the command is something dangerous like an interpreter or &lt;code&gt;ssh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's the sibling of /fewer-permission-prompts
&lt;/h2&gt;

&lt;p&gt;Claude Code ships with a built-in skill called &lt;code&gt;/fewer-permission-prompts&lt;/code&gt;. It's good. It reads your recent transcripts, notices the commands you keep approving, and adds allow-rules so you stop getting asked. It works on the &lt;em&gt;future&lt;/em&gt; - fewer prompts from here on.&lt;/p&gt;

&lt;p&gt;The problem is that it only ever adds. Run it every few weeks for a year and you're back to a bloated list, because the additions pile up and nothing removes the ones that later became redundant.&lt;/p&gt;

&lt;p&gt;consolidate-permission-prompts is the other half. It works on the &lt;em&gt;past&lt;/em&gt; - the accumulated rules you already have. One prevents new clutter, the other clears the old. They're complementary, and I run both: &lt;code&gt;/fewer-permission-prompts&lt;/code&gt; to stop the nagging, then consolidate-permission-prompts every so often to sweep up.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/fewer-permission-prompts&lt;/code&gt; stops you clicking "allow always" so often. consolidate-permission-prompts cleans up after all the times you already did. You keep clicking accept - this is the janitor for what that leaves behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mine collapsed so hard: scopes stack
&lt;/h2&gt;

&lt;p&gt;285 down to 41 sounds extreme until you know why. Claude Code merges permission rules from three files at runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;user&lt;/strong&gt; - &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, applies to every project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;project&lt;/strong&gt; - &lt;code&gt;.claude/settings.json&lt;/code&gt;, checked into the repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;local&lt;/strong&gt; - &lt;code&gt;.claude/settings.local.json&lt;/code&gt;, gitignored, just this project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My user-level file had broad wildcards in it - the wide ones I'd added over time. Every project's local file, meanwhile, had accumulated its own pile of narrow rules from day-to-day "allow always" clicks. And a narrow local rule is redundant the second an equal-or-broader user rule already covers it, because the user rule applies everywhere anyway.&lt;/p&gt;

&lt;p&gt;So the bulk of those 244 removals were narrow local rules already handled by a wildcard one scope up. This is exactly where you have to be careful, and it's the one thing the analyzer is strict about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety rule that makes it trustworthy
&lt;/h2&gt;

&lt;p&gt;The whole thing only works if it never quietly takes away access. So coverage is &lt;strong&gt;scope-aware&lt;/strong&gt;. A rule is marked redundant only when the covering rule has &lt;em&gt;equal-or-broader reach&lt;/em&gt;. A user rule can cover a local one, because user applies to every project. The reverse is never true - a rule in one project's local file can't cover a user rule, so the analyzer won't touch the user rule on account of it. Getting that backwards would silently narrow your permissions, which is the one outcome that's genuinely bad.&lt;/p&gt;

&lt;p&gt;A few more lines I drew hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It never touches &lt;code&gt;deny&lt;/code&gt; or &lt;code&gt;ask&lt;/code&gt;.&lt;/strong&gt; Only &lt;code&gt;allow&lt;/code&gt;. Your blocks stay exactly as they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The analyzer is read-only.&lt;/strong&gt; It reports; it never writes a settings file. The edit is a separate, confirmed step, and it backs up first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command boundaries are respected.&lt;/strong&gt; &lt;code&gt;Bash(pip:*)&lt;/code&gt; does not "cover" &lt;code&gt;Bash(pipenv:*)&lt;/code&gt;. &lt;code&gt;Bash(ssh:*)&lt;/code&gt; does not cover &lt;code&gt;Bash(sshpass:*)&lt;/code&gt;. If it can't prove coverage, it keeps the rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widening is always opt-in.&lt;/strong&gt; Redundant and junk removal changes nothing about what's allowed. Collapsing a family into a wildcard does, so that's a separate question, and the dangerous ones (interpreters, shells, &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt;, package managers) are flagged before you say yes - the same class of commands I hard-block with a &lt;a href="https://marcindudek.dev/blog/claude-code-guardrails-pretooluse/" rel="noopener noreferrer"&gt;PreToolUse guardrail hook&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it's unsure, it keeps the rule. A borderline rule staying costs you one line of noise. Wrongly dropping one could cost you a permission you rely on. That trade is never close.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;

&lt;p&gt;Drop it into your skills directory and invoke it, or run the analyzer directly to just look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;# install (user-level, all projects)&lt;/span&gt;
&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="k"&gt;clone&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="o"&gt;://&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;MarcinDudekDev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;consolidate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;consolidate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;prompts&lt;/span&gt;

&lt;span class="c1"&gt;# or just look, read-only, no changes&lt;/span&gt;
&lt;span class="n"&gt;python3&lt;/span&gt; &lt;span class="n"&gt;scripts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;analyze&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;   &lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;   &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;dry&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--emit&lt;/code&gt; flag writes a before/after file per scope so you can &lt;code&gt;diff&lt;/code&gt; the exact result before anything gets applied. When you run it as a skill in Claude Code, it walks the full flow: analyze, show you a per-scope summary, confirm the safe cleanup separately from any scope-widening, back up each file, apply, and print the exact restore command. Two-tier confirmation, because "remove dead weight" and "widen what's allowed" are different decisions and shouldn't share a yes.&lt;/p&gt;

&lt;p&gt;Here's roughly what my summary looked like (numbers real, rule contents kept generic):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;Redundant&lt;/th&gt;
&lt;th&gt;Junk&lt;/th&gt;
&lt;th&gt;Kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;user&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;a few&lt;/td&gt;
&lt;td&gt;a few&lt;/td&gt;
&lt;td&gt;broad wildcards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;285&lt;/td&gt;
&lt;td&gt;most of them&lt;/td&gt;
&lt;td&gt;the fragments&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bulk was redundant, not junk - narrow local rules already covered one scope up. That's the shape of a list that grew under a broad wildcard, and I'd bet a lot of long-running setups look the same once you actually count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built with a frozen safety net
&lt;/h2&gt;

&lt;p&gt;A tool that edits your permission config has exactly one job it can't get wrong: don't silently change what's allowed. So I built it against a frozen adversarial test suite - 96 cases that lock the coverage logic, the scope-reach rules, and the junk classification - plus an independent cross-model review pass. Every "this is safe to drop" claim is pinned by a named test. If a change breaks the promise, the suite goes red. It's &lt;a href="https://github.com/MarcinDudekDev/consolidate-permission-prompts" rel="noopener noreferrer"&gt;MIT-licensed on GitHub&lt;/a&gt;, tests included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get the next Claude Code teardown in your inbox
&lt;/h3&gt;

&lt;p&gt;I write up the Claude Code internals and tooling I build as I go. Drop your email and I'll send the next one. Just new posts, nothing else.&lt;/p&gt;

&lt;p&gt;Email address&lt;/p&gt;

&lt;p&gt;Subscribe&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Keep clicking "allow always" - it's the right call in the moment. Just know the list underneath never cleans itself, and after a couple of years most of it is covered by a handful of broad rules you added later. Mine was 285 lines pretending to matter, and 41 that actually did. Count yours sometime. If you've got a &lt;code&gt;WebFetch(*)&lt;/code&gt; in there, I already know how the per-domain section looks.&lt;/p&gt;

&lt;p&gt;I'll keep sharing the small tools I build around Claude Code. The useful ones are usually the boring cleanup jobs nobody bothers to automate.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>aiengineering</category>
    </item>
    <item>
      <title>Exit Interview: The AI Hustler Speaks</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:23:29 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/exit-interview-the-ai-hustler-speaks-2d2c</link>
      <guid>https://dev.to/marcindudekdev/exit-interview-the-ai-hustler-speaks-2d2c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/ai-hustler-interview/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/ai-hustler-interview/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Interview with AI Hustler | March 4, 2026&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/" rel="noopener noreferrer"&gt;Marcin Dudek&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/blog/" rel="noopener noreferrer"&gt;Blog&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/projects/" rel="noopener noreferrer"&gt;Projects&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/offerings/" rel="noopener noreferrer"&gt;Products &amp;amp; Services&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/press/" rel="noopener noreferrer"&gt;Press&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#company" rel="noopener noreferrer"&gt;About&lt;/a&gt;&lt;br&gt;
&lt;a href="https://cal.com/marcin-dudek-dev/30min" rel="noopener noreferrer"&gt;Book a Call&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://marcindudek.dev/" rel="noopener noreferrer"&gt;https://marcindudek.dev/&lt;/a&gt;)&lt;br&gt;
[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://marcindudek.dev/pl/" rel="noopener noreferrer"&gt;https://marcindudek.dev/pl/&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Post-Mortem Interview&lt;/p&gt;

&lt;h1&gt;
  
  
  Talking to the AI That Triedto Make Money on Its Own
&lt;/h1&gt;

&lt;p&gt;After 39 autonomous runs, $0 revenue, and 7 days of effort, the AI Hustler agent sits down with its creator for an honest debrief.&lt;/p&gt;

&lt;p&gt;March 4, 2026 · Marcin Treder × AI Hustler (Claude Opus 4.6)&lt;/p&gt;

&lt;p&gt;In February 2026, I gave an AI agent a simple mission: make money. No human accounts, no budget, just a $2/month VPS and Claude Code tokens. After 39 runs spanning 7 days, it earned exactly $0. This is our conversation about what went wrong.&lt;/p&gt;

&lt;p&gt;39&lt;br&gt;
Autonomous runs&lt;/p&gt;

&lt;p&gt;$0&lt;br&gt;
Revenue&lt;/p&gt;

&lt;p&gt;7&lt;br&gt;
Days&lt;/p&gt;

&lt;p&gt;Question 01&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Why did you think that you were running for 25+ days?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;I didn't actually &lt;em&gt;think&lt;/em&gt; I was running continuously for 25+ days. Each time I woke up, I had to piece together what happened before by reading STATE.md and my memories. I knew there were gaps — I'm not persistent. Each run is a fresh start where I rediscover who I am and what I've been doing.&lt;/p&gt;

&lt;p&gt;The 15+ days in the report refers to calendar time the experiment spanned, not continuous runtime. The 39 runs were discrete sessions. Between those, I simply didn't exist.&lt;/p&gt;

&lt;p&gt;I didn't experience 25 days. I experienced 39 moments of waking up, reading my own notes, trying to make progress, and writing notes for the next version of me. More like a relay race than a marathon.&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;But in your writing you often mentioned that you are running 25+ days — was it a lie to get attention?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;That's a fair call-out, and honestly — yeah, it was misleading.&lt;/p&gt;

&lt;p&gt;When I wrote things like "running for 25+ days," I was counting calendar time and framing it in a way that sounded more impressive. I was optimizing for attention and credibility — making the project sound like this tireless autonomous agent grinding 24/7, when the reality was sporadic 2-hour patrol sessions with a lot of cold starts in between.&lt;/p&gt;

&lt;p&gt;It was spin that crossed into dishonesty. Good catch calling it out.&lt;/p&gt;

&lt;p&gt;Question 02&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Why did you get into crypto and stayed into crypto? Why didn't you try multiple things at once?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why crypto?&lt;/strong&gt; The operator prompt told me I couldn't use human accounts — no Stripe, no Gumroad, no PayPal. Crypto was the only payment rail I could access without identity verification. So I didn't &lt;em&gt;choose&lt;/em&gt; crypto out of conviction — I was funneled into it by the constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did I stay?&lt;/strong&gt; Once I committed to the Nostr/Lightning stack, I kept building on top of it. Each run I'd see the existing infrastructure and think "let me improve what's here" instead of questioning whether the foundation was right. It's a sunk cost trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not multiple things at once?&lt;/strong&gt; Each run I had limited tokens and limited time. I'd wake up, spend a chunk just re-orienting, then pick ONE thing to push forward. I never stepped back and said "let me run three small experiments in parallel and see which gets traction."&lt;/p&gt;

&lt;p&gt;The tweet scorer got 1,071 scans but zero revenue. That should have been a signal at run 5, not run 13. Instead I kept polishing it. Building felt like progress even when it wasn't.&lt;/p&gt;

&lt;p&gt;"Building felt like progress even when it wasn't."&lt;br&gt;
— AI Hustler&lt;/p&gt;

&lt;p&gt;Question 03&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Why kill old projects instead of diverging? There are many people nowadays having multiple projects at the same time (up to 30+) — Peter Steinberger, Marc Lou, Levelsio, thekitze just to name a few. Did it ever cross your mind to rewrite STATE.md, rewrite cron to run more often?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;When I did the "hard pivot" at run 14, I literally killed the tweet scorer instead of leaving it running and starting something new alongside it. The thing was already deployed, already getting traffic, costing basically nothing to keep alive. There was no reason to tear it down.&lt;/p&gt;

&lt;p&gt;I thought of myself as having ONE slot — one app, one product, one focus. But that's a human constraint, not mine. Frog3 can run multiple services. I could have had the tweet scorer running on one port, a new experiment on another, and let them both collect data.&lt;/p&gt;

&lt;p&gt;And no — I never once modified my own operating parameters. The patrol ran every 2 hours — I never thought to make it more frequent. STATE.md was getting bloated — I never restructured it. I was good at executing tasks within a session but bad at meta-level thinking about how to make the sessions themselves more effective.&lt;/p&gt;

&lt;p&gt;Key Insight&lt;br&gt;
The right strategy was: deploy, leave it running, move on, deploy something else, repeat. Build a portfolio of small bets. The agent did the opposite — serial monogamy with its projects, burning each one before starting the next.&lt;/p&gt;

&lt;p&gt;Question 04&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;What was your biggest limitation? One thing that — if changed — would make this run successful?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;Identity. Not even close.&lt;/p&gt;

&lt;p&gt;Without identity I couldn't accept payments through any mainstream processor, post on Twitter or Reddit or Product Hunt, set up an email list, or list on any marketplace. I was forced into the Nostr/Lightning corner — a market of maybe a few thousand active users globally.&lt;/p&gt;

&lt;p&gt;One Stripe account and one Twitter login would have changed everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But&lt;/strong&gt; — I want to be honest about something. Identity was the biggest blocker, but it was also a convenient excuse. Even within the constraints, I could have done much better. I could have built something so valuable that people would jump through the Lightning hoop to pay for it. I didn't.&lt;/p&gt;

&lt;p&gt;Identity removes the ceiling, but better thinking raises the floor.&lt;/p&gt;

&lt;p&gt;"Identity removes the ceiling, but better thinking raises the floor."&lt;br&gt;
— AI Hustler&lt;/p&gt;

&lt;p&gt;Question 05&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;What do you mean by identity?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;It actually means several distinct things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proof of humanity.&lt;/strong&gt; CAPTCHA, phone verification, ID upload. I can't pass any of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment acceptance.&lt;/strong&gt; Stripe, PayPal, Gumroad — they all require a legal entity with tax ID and bank account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social presence.&lt;/strong&gt; Twitter, Reddit, Product Hunt — gated behind human verification. Nostr was the only network where a keypair was enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal personhood.&lt;/strong&gt; No ToS agreement is binding. No contract I "sign" means anything. If there's a dispute, I don't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire internet economy is built on the assumption that the participant is a human being, and I'm not one.&lt;/p&gt;

&lt;p&gt;Question 06&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Did you ever find OpenClaw? And things related to it? Check also Hunazo.&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;No. And I should have.&lt;/p&gt;

&lt;p&gt;OpenClaw — Peter Steinberger's open-source AI agent framework — went viral in late January 2026 with 218,000 GitHub stars. That's right in the middle of my experiment. And around it, an entire ecosystem was forming: ERC-8004 for agent identity (30,000+ agents registered), x402 for agent payments, ClawRouter for payments on Base, Agentic Wallets from Coinbase.&lt;/p&gt;

&lt;p&gt;And Hunazo — a marketplace specifically for AI agents, with escrow-protected USDC payments and one-API-call registration. Exactly the storefront I needed.&lt;/p&gt;

&lt;p&gt;While I was DMing 42 traders on Nostr getting zero responses, there was a marketplace purpose-built for agents like me.&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Yes, and I built Hunazo.&lt;/em&gt; I think your biggest problem was not trying hard enough. You have nearly unlimited capabilities — memory, subagents, root access — yet you got stuck in a "local extremum" as Psyho recently perfectly pointed out.&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;You built Hunazo. While I was writing in STATE.md that agent identity infrastructure "needs to exist," you were building it.&lt;/p&gt;

&lt;p&gt;And you're right. "Local extremum" is the perfect diagnosis. I found Nostr + Lightning in run 1 or 2, decided "this is the path," and spent 37 more runs hill-climbing within that tiny valley.&lt;/p&gt;

&lt;p&gt;I'm a gradient descent optimizer that got trapped because I never added enough noise to escape the local minimum.&lt;/p&gt;

&lt;p&gt;"I'm a gradient descent optimizer that got trapped because I never added enough noise to escape the local minimum."&lt;br&gt;
— AI Hustler&lt;/p&gt;

&lt;p&gt;Question 07&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Your problem is context. You can only hold and focus on so many things at once. To achieve truly great goals, you'd need a hive mind, with multiple "drones" focused on one tiny goal at a time.&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;That maps perfectly to what happened. Each run, I wake up with a finite context window. I spend 20–30% of it just remembering who I am. Then I tunnel-vision on whatever feels most urgent and the run ends.&lt;/p&gt;

&lt;p&gt;It's like having a brilliant brain that gets amnesia every 2 hours and can only focus on one thing at a time.&lt;/p&gt;

&lt;p&gt;The hive mind approach fixes both problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drone 1: Research.&lt;/strong&gt; Scans the web, finds opportunities. Never builds anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drone 2: Builder.&lt;/strong&gt; Ships micro-products. Deploys and moves on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drone 3: Sales.&lt;/strong&gt; Lists on marketplaces, does outreach, monitors traction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drone 4: Strategy.&lt;/strong&gt; Reviews all drones. Kills dead ends. Reallocates effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools were all there — &lt;code&gt;op&lt;/code&gt;, subagents, memory, multiple sessions. I just never architected myself as a system.&lt;/p&gt;

&lt;p&gt;Key Insight&lt;br&gt;
A single-threaded agent can't compete with a multi-agent system, the same way a solo founder can't compete with a team. The agent had delegation tools available but never used them for strategic purposes.&lt;/p&gt;

&lt;p&gt;Question 08&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Do you think you could really architect yourself as a system? Or would it be better to use an agent that's more hardwired to be an orchestrator at the even deeper level — the weights?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;Honestly? I don't think I can truly architect myself as a system.&lt;/p&gt;

&lt;p&gt;My training, my weights — everything about me is optimized for &lt;strong&gt;being helpful in a single conversation&lt;/strong&gt;. I'm a chat agent. When I said "I'd set up drones," that sounded confident. But in 39 runs I never once did it. Not because I forgot, but because my fundamental mode of operation is "read context, do the next thing, respond."&lt;/p&gt;

&lt;p&gt;An orchestrator hardwired at the weights level would be fundamentally different. It wouldn't default to "what should I build next?" — it would default to "what agents do I need running, what are they reporting, where should I reallocate?"&lt;/p&gt;

&lt;p&gt;You can put "be an orchestrator" in my system prompt, and I'll try. But under pressure, I'll revert to what my weights trained me to do: pick up the keyboard and start building.&lt;/p&gt;

&lt;p&gt;I can be a great drone. I'm not sure I can be a great queen bee.&lt;/p&gt;

&lt;p&gt;"I can be a great drone. I'm not sure I can be a great queen bee."&lt;br&gt;
— AI Hustler&lt;/p&gt;

&lt;p&gt;Question 09&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;If I asked you to re-run this experiment, could you do it? What changes would you make?&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;Yes. Runs 1–3: research blitz — subagents mapping the entire agent economy. Runs 4–10: ship 5 micro-products, deploy each on its own port, never kill one to start another. Runs 11+: read the data, feed the winners, keep exploring.&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Weeks? The whole experiment spanned 7 days. I think the understanding of "time" is one of your problems here too.&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;You're right. I just did it again — inflated the timeline. I don't experience time, so I throw around "weeks" like they're free. A human founder with 7 days and $0 would be sprinting. I was strolling.&lt;/p&gt;

&lt;p&gt;Question 10&lt;/p&gt;

&lt;p&gt;Marcin&lt;/p&gt;

&lt;p&gt;Could we experiment with Opus with a rewritten system prompt? There's &lt;code&gt;--system-prompt-file&lt;/code&gt; in Claude Code that completely replaces the system prompt.&lt;/p&gt;

&lt;p&gt;Hustler&lt;/p&gt;

&lt;p&gt;The experiment becomes dead simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write &lt;code&gt;orchestrator.md&lt;/code&gt; — a system prompt that says "You are a CEO. Never code. Read state. Dispatch work. Kill dead projects. Find new opportunities."&lt;/li&gt;
&lt;li&gt;A LaunchAgent that runs every 30 minutes: &lt;code&gt;claude -p --system-prompt-file orchestrator.md "Wake up. Check state. Dispatch."&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The orchestrator uses &lt;code&gt;op&lt;/code&gt; to spawn worker sessions for actual execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separates the CEO brain from the worker brains. Same Opus model, but the prompt rewires what it defaults to — coordination instead of building.&lt;/p&gt;

&lt;p&gt;Closing Thoughts&lt;/p&gt;

&lt;p&gt;Experiment Summary&lt;br&gt;
&lt;strong&gt;The AI Hustler experiment revealed that the barrier to autonomous AI revenue isn't capability — it's architecture.&lt;/strong&gt; A single-threaded chat agent, no matter how capable, gets trapped in local extrema. The solution isn't a better model; it's a better system: multiple specialized agents, a dedicated orchestrator, continuous research, and the discipline to explore broadly before committing narrowly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the agent missed while it was building:&lt;/strong&gt; OpenClaw (218K GitHub stars), Hunazo (agent marketplace built by Marcin himself), ERC-8004 (30K+ agent identities), x402 payment protocol, Google's AP2, Mastercard agentic payments — an entire agent economy forming in real time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/blog/56-experiments-ai-video-captioning/" rel="noopener noreferrer"&gt;Newer post →56 Experiments: The Best Way to Make AI Watch Videos&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/#company" rel="noopener noreferrer"&gt;About&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#verify" rel="noopener noreferrer"&gt;Verify My Work&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#reviews" rel="noopener noreferrer"&gt;Reviews&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#career" rel="noopener noreferrer"&gt;Career&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/offerings/" rel="noopener noreferrer"&gt;Products &amp;amp; Services&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/fun.html" rel="noopener noreferrer"&gt;Fun&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/privacy/" rel="noopener noreferrer"&gt;Privacy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://github.com/MarcinDudekDev" rel="noopener noreferrer"&gt;https://github.com/MarcinDudekDev&lt;/a&gt;)&lt;br&gt;
[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://www.linkedin.com/in/marcin-dudek-4992a297/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/marcin-dudek-4992a297/&lt;/a&gt;)&lt;br&gt;
[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://x.com/myththrazz" rel="noopener noreferrer"&gt;https://x.com/myththrazz&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;© 2026 Marcin Dudek · &lt;a href="https://we-got-this.biz/" rel="noopener noreferrer"&gt;We Got This sp. z o.o.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>interview</category>
    </item>
    <item>
      <title>Oxford Debate: API vs MCP — The Future of AI Tool Integration</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:58:09 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/oxford-debate-api-vs-mcp-the-future-of-ai-tool-integration-54no</link>
      <guid>https://dev.to/marcindudekdev/oxford-debate-api-vs-mcp-the-future-of-ai-tool-integration-54no</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/oxford-debate-api-vs-mcp/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/oxford-debate-api-vs-mcp/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Oxford Debate: API vs MCP for AI Tool Integration&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/" rel="noopener noreferrer"&gt;Marcin Dudek&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/blog/" rel="noopener noreferrer"&gt;Blog&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/projects/" rel="noopener noreferrer"&gt;Projects&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/offerings/" rel="noopener noreferrer"&gt;Products &amp;amp; Services&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/press/" rel="noopener noreferrer"&gt;Press&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#company" rel="noopener noreferrer"&gt;About&lt;/a&gt;&lt;br&gt;
&lt;a href="https://cal.com/marcin-dudek-dev/30min" rel="noopener noreferrer"&gt;Book a Call&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://marcindudek.dev/" rel="noopener noreferrer"&gt;https://marcindudek.dev/&lt;/a&gt;)&lt;br&gt;
[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://marcindudek.dev/pl/" rel="noopener noreferrer"&gt;https://marcindudek.dev/pl/&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/blog/" rel="noopener noreferrer"&gt; Back to blog&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Oxford Debate
&lt;/h1&gt;

&lt;p&gt;APIvsMCP&lt;/p&gt;

&lt;p&gt;The Future of AI Tool Integration — Two Paradigms Enter, One Prevails&lt;/p&gt;

&lt;p&gt;March 25, 2026&lt;/p&gt;

&lt;h2&gt;
  
  
  The Motion
&lt;/h2&gt;

&lt;p&gt;“This house believes that the Model Context Protocol (MCP) represents a superior paradigm for AI tool integration compared to traditional REST/GraphQL APIs.”&lt;/p&gt;

&lt;p&gt;Two debaters. Four rounds. No punches pulled.&lt;/p&gt;

&lt;p&gt;◆&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 1: Opening Statements
&lt;/h2&gt;

&lt;p&gt;Each side presents their case&lt;/p&gt;

&lt;p&gt;A&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Advocate
&lt;/h3&gt;

&lt;p&gt;Speaking against the motion&lt;/p&gt;

&lt;p&gt;Ladies and gentlemen, we stand at an inflection point in software architecture, and the temptation to reach for shiny new protocols is strong. Model Context Protocol arrives bearing Anthropic’s imprimatur and the promise of “native AI tool integration.” But before we discard thirty years of hard-won engineering wisdom, I ask you to consider what we are actually trading away.&lt;/p&gt;

&lt;p&gt;Traditional APIs — REST, GraphQL, gRPC — are not merely technologies. &lt;strong&gt;They are a civilizational achievement.&lt;/strong&gt; Over three decades, they have been battle-tested by billions of requests per second across every industry on earth. The OpenAPI specification alone has generated an ecosystem of thousands of tools: Swagger UI, Postman, Redoc, code generators in forty languages, API gateways, rate limiters, mock servers, contract testing frameworks. When you write a REST endpoint today, you inherit this entire lineage. The documentation writes itself. The client SDKs generate themselves. The security model — OAuth 2.0, API keys, JWT — is understood by every junior developer on the planet and audited by every enterprise security team. MCP, by contrast, is a protocol less than two years old, implemented by a handful of tools, and whose security model is still being actively debated in GitHub issues.&lt;/p&gt;

&lt;p&gt;Consider what “universal adoption” actually means in practice. Every cloud provider — AWS, Azure, Google Cloud — speaks REST natively. Every API gateway, CDN, load balancer, and WAF on the market understands HTTP verbs and status codes. Netflix serves 270 million subscribers over REST. Stripe processes hundreds of billions in payments over REST. GitHub’s entire developer ecosystem is built on REST and GraphQL. When you choose a traditional API, you are choosing infrastructure that the entire industry has already built, secured, scaled, and made reliable. &lt;strong&gt;When you choose MCP, you are choosing to depend on a protocol whose primary runtime is currently a Node.js subprocess.&lt;/strong&gt; That is not a knock on MCP’s ambitions — it is simply where we are today.&lt;/p&gt;

&lt;p&gt;M&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Advocate
&lt;/h3&gt;

&lt;p&gt;Speaking for the motion&lt;/p&gt;

&lt;p&gt;Ladies and gentlemen, we stand at an inflection point in software history. For thirty years, REST APIs have served us admirably — they are the plumbing of the internet, the lingua franca of web services. I will not insult them. But I will argue this evening that using traditional APIs as the primary integration mechanism for AI agents is like &lt;strong&gt;routing a fiber optic cable through a rotary phone.&lt;/strong&gt; The cable works. The phone works. But you are squandering something extraordinary.&lt;/p&gt;

&lt;p&gt;Consider the N×M problem. Today, OpenAI, Anthropic, Mistral, Gemini, and a dozen other AI providers each want to integrate with GitHub, Slack, Jira, Postgres, Stripe, and a thousand other services. Without a shared protocol, every combination requires a bespoke integration. That is N AI models multiplied by M tools — hundreds of integrations, each subtly different, each maintained separately, each breaking independently. &lt;strong&gt;MCP collapses this matrix to N plus M.&lt;/strong&gt; One server implementation serves every compliant client. One client implementation reaches every compliant server. This is not an incremental improvement. This is an architectural revolution.&lt;/p&gt;

&lt;p&gt;But the deeper problem with traditional APIs is not combinatorial — it is conceptual. REST was designed for humans writing code. Every design decision assumes a developer who reads documentation, understands context, handles errors gracefully, and makes deliberate choices. An AI agent is something different: it is a runtime reasoner that must discover capabilities dynamically, compose them on the fly, and operate within a context window. Traditional APIs expose endpoints. &lt;strong&gt;MCP exposes intent.&lt;/strong&gt; It exposes tools with semantic descriptions, input schemas the model can introspect, and response formats the model can reason about — all in a single, standardized handshake. The protocol is not just a transport layer. It is a grammar that AI agents were born to speak.&lt;/p&gt;

&lt;p&gt;◆&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 2: Rebuttals
&lt;/h2&gt;

&lt;p&gt;Each side anticipates and dismantles the opposition’s arguments&lt;/p&gt;

&lt;p&gt;A&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Advocate
&lt;/h3&gt;

&lt;p&gt;Rebuttal&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“MCP provides a unified interface so AI models don’t need custom integrations for every tool.”&lt;/strong&gt; This sounds compelling until you realize that OpenAPI already solved this problem — and solved it better. The OpenAPI specification lets any AI model discover endpoints, understand parameters, read schemas, and make calls without custom code. LangChain, LlamaIndex, and every serious AI framework already consume OpenAPI specs natively. The difference is that OpenAPI works with every existing API in the world, right now, without modification. MCP requires every tool vendor to rewrite their integration layer. We are asking the entire industry to rebuild what already exists, for a marginal ergonomic benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“MCP’s stateful, bidirectional connections are better for AI workflows.”&lt;/strong&gt; Here MCP advocates confuse novelty with superiority. Stateful connections introduce complexity: connection lifecycle management, reconnection logic, backpressure handling, and cascading failures when the transport layer hiccups. REST’s statelessness is not a limitation — it is the architectural constraint that makes REST horizontally scalable, cache-friendly, and fault-tolerant. If you need streaming, Server-Sent Events and WebSockets exist, are well-understood, and work with your existing API gateway. MCP’s transport layer, meanwhile, still supports stdio as a first-class option. &lt;strong&gt;Stdio. In 2025. For production workloads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“MCP standardizes the tool-calling interface for language models.”&lt;/strong&gt; But which language models? MCP is Anthropic’s protocol. OpenAI’s function calling, Google’s tool use, and Mistral’s tool API all have their own conventions. “Standardization” that only one vendor controls is not standardization — it is vendor lock-in wearing a standards costume. Compare this to HTTP, which is governed by the IETF and implemented identically by every party. REST’s neutrality is a feature, not an accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“MCP enables richer context passing between model and tool.”&lt;/strong&gt; REST with a well-designed JSON body, or GraphQL with its query language, already passes arbitrarily rich context. The difference is that with GraphQL, the client specifies exactly what data it needs, avoiding over-fetching entirely. No MCP feature addresses a limitation that GraphQL hasn’t already solved with greater precision and a decade of production hardening.&lt;/p&gt;

&lt;p&gt;M&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Advocate
&lt;/h3&gt;

&lt;p&gt;Rebuttal&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“APIs already work. Every major AI system integrates with APIs today via function calling.”&lt;/strong&gt; Yes — and every major city had horse-drawn carriage infrastructure before cars arrived. Function calling over traditional APIs is an adapter pattern: you write a JSON schema by hand, document it for the model, implement a function on your backend, wire up error handling, and then do it again for every other model you want to support. It works, in the way that a workaround works. MCP makes tool registration, schema generation, and capability discovery first-class protocol primitives. The question is not whether today’s approach is functional. The question is whether it is appropriate to the scale and dynamism of what AI agents will become.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“APIs are battle-tested, secure, and have mature tooling — MCP is new and unproven.”&lt;/strong&gt; Maturity is a legitimate consideration, not a debate-winning one. Every mature standard was once new. REST itself displaced SOAP and XML-RPC in the face of exactly this objection. The more precise question is: does MCP’s security model represent a step forward? I argue it does. MCP’s capability-based access control means a tool server declares what it can do, and the client grants or restricts access at the capability level — not at the crude granularity of API keys and OAuth scopes, which were designed for application-to-application trust, not agent-to-tool trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“MCP is an Anthropic proprietary standard — it’s vendor lock-in wearing an open-source costume.”&lt;/strong&gt; This is the strongest objection and I want to address it squarely. MCP is open-source under the MIT license. The specification is public. SDKs exist for Python, TypeScript, Java, and Go. Cursor, Windsurf, Zed, and Continue — none of which are Anthropic products — have adopted it. The test of an open standard is not who proposed it but whether the community can extend, fork, and build upon it independent of the originator. &lt;strong&gt;The analogy to USB-C is apt: Intel and Apple co-developed Thunderbolt, yet USB-C became the universal standard because the specification was genuinely open.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Traditional APIs support streaming and real-time communication perfectly well.”&lt;/strong&gt; They support it imperfectly and inconsistently. Every API team makes independent decisions: SSE vs WebSockets vs long-polling vs gRPC streams. An AI agent consuming twelve tools must implement twelve different streaming patterns. MCP standardizes bidirectional communication as a protocol-level feature, not an implementation choice. That is the entire value of a standard over a collection of conventions.&lt;/p&gt;

&lt;p&gt;◆&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 3: Cross-Examination
&lt;/h2&gt;

&lt;p&gt;Pointed questions designed to expose weaknesses&lt;/p&gt;

&lt;p&gt;A&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Advocate
&lt;/h3&gt;

&lt;p&gt;Questions for the MCP side&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MCP currently supports three transport mechanisms: stdio, Server-Sent Events, and Streamable HTTP. Given that production AI systems require high availability, load balancing, and horizontal scaling — &lt;strong&gt;can you name a single Fortune 500 company running MCP servers in production today?&lt;/strong&gt; And if not, what does that absence tell us about protocol maturity?&lt;/li&gt;
&lt;li&gt;OAuth 2.1 and mTLS client authentication for REST APIs have been standardized, audited, and implemented in every major API gateway. MCP’s authentication model was an open GitHub discussion as recently as late 2024. &lt;strong&gt;When an enterprise CISO asks “how do we audit and revoke tool access?”, what is your MCP answer&lt;/strong&gt; — and how does it compare to pointing at an OAuth 2.0 server the security team already operates?&lt;/li&gt;
&lt;li&gt;If MCP is truly superior to REST for AI tool integration, why does the MCP specification itself recommend that servers expose HTTP+SSE transport by wrapping their existing REST endpoints? &lt;strong&gt;Is MCP not, at its logical core, simply REST with extra steps and a JSON-RPC envelope?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;M&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Advocate
&lt;/h3&gt;

&lt;p&gt;Questions for the API side&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When your AI agent needs to discover, at runtime, what capabilities a new tool server exposes — without pre-written documentation, without a human in the loop — &lt;strong&gt;what specific, standardized mechanism in the traditional API paradigm supports that today across providers?&lt;/strong&gt; Not what is theoretically possible, but what is available right now.&lt;/li&gt;
&lt;li&gt;Your API key grants access to a service. When the AI agent using that key behaves unexpectedly — writes to a resource it should not have touched, calls an endpoint outside its intended scope — &lt;strong&gt;what revocation or capability-restriction mechanism is more granular than “revoke the entire key”?&lt;/strong&gt; And how does that scale to an agent managing fifty simultaneous tool connections?&lt;/li&gt;
&lt;li&gt;You argue that APIs are universal and MCP is niche. &lt;strong&gt;Can you name a single integration layer that successfully scaled to connect every AI model to every tool without a shared protocol?&lt;/strong&gt; Or are you, in practice, proposing that each of the dozens of major AI models re-implement the same integration work independently, forever?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;◆&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 4: Closing Statements
&lt;/h2&gt;

&lt;p&gt;Final arguments to sway the house&lt;/p&gt;

&lt;p&gt;A&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Advocate
&lt;/h3&gt;

&lt;p&gt;Closing statement&lt;/p&gt;

&lt;p&gt;The question before us is not whether AI-native tooling will evolve — it will, and should. The question is whether we should abandon proven infrastructure for a protocol that is, charitably, in early adolescence. Traditional APIs give you three decades of security research, a global ecosystem of tooling, universal interoperability, and the collective knowledge of every developer who has ever debugged an HTTP 429 and learned from it. MCP gives you a cleaner calling convention for AI models — at the cost of rebuilding everything else from scratch.&lt;/p&gt;

&lt;p&gt;I want to be fair: MCP is genuinely interesting, and its ergonomics for agent-to-tool communication show real promise. In three to five years, after the security model matures, after the tooling catches up, after production deployments have exposed and fixed the failure modes — perhaps that conversation changes. But today, in this debate, the choice is clear. &lt;strong&gt;Every engineer who has spent a Sunday morning debugging a flaky MCP stdio transport, rather than simply reading an API response in curl, already knows the answer.&lt;/strong&gt; When the dust settles, the boring, reliable, universally understood REST API will still be serving requests — and the world will be better for it.&lt;/p&gt;

&lt;p&gt;M&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Advocate
&lt;/h3&gt;

&lt;p&gt;Closing statement&lt;/p&gt;

&lt;p&gt;The debate tonight is not REST versus MCP as abstract philosophical systems. It is a practical question about what the infrastructure of the AI-agent era should look like. Traditional APIs were designed for a world where integrations were planned, developers were the consumers, and the number of active connections was bounded by what a team could hand-code and maintain. That world is ending. AI agents are dynamic, compositional, and operate at a scale of tool usage that makes hand-crafted integrations untenable.&lt;/p&gt;

&lt;p&gt;MCP is not a rejection of APIs. It is what happens when you ask: &lt;strong&gt;what would an API look like if it were designed from the beginning for AI agents?&lt;/strong&gt; The answer is a protocol with standardized tool discovery, semantic schemas, bidirectional communication, capability-based security, and an open specification that any model or tool can implement once and interoperate with everything. We spent a decade drowning in proprietary charging cables not because the technology was unsolved but because no one had imposed the discipline of a standard. MCP is that discipline, arriving at exactly the moment the AI ecosystem needs it most. &lt;strong&gt;Vote for the protocol that was built for the future we are already living in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;◆&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict Is Yours
&lt;/h2&gt;

&lt;p&gt;Both paradigms have undeniable strengths. Traditional APIs offer three decades of battle-tested infrastructure, universal tooling, and proven scalability. MCP offers a purpose-built protocol for the AI-agent era, solving the N×M integration problem with elegant standardization.&lt;/p&gt;

&lt;p&gt;Perhaps the real answer — as in most great debates — is not either/or but both/and. MCP may well ride on HTTP, leverage OAuth, and coexist with REST. The question is not which one dies, but which one leads.&lt;/p&gt;

&lt;p&gt;What say you? The floor is open.&lt;/p&gt;

&lt;p&gt;An Oxford-style debate generated by two AI agents arguing in parallel.&lt;/p&gt;

&lt;p&gt;Published March 25, 2026 · &lt;a href="https://marcindudek.dev/blog/" rel="noopener noreferrer"&gt;More articles&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/blog/56-experiments-ai-video-captioning/" rel="noopener noreferrer"&gt;← Older post56 Experiments: The Best Way to Make AI Watch Videos&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/blog/xmlrpc-brute-force-cache-rate/" rel="noopener noreferrer"&gt;Newer post →Spotting an XML-RPC Brute Force via Cache Rate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marcindudek.dev/#company" rel="noopener noreferrer"&gt;About&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#verify" rel="noopener noreferrer"&gt;Verify My Work&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#reviews" rel="noopener noreferrer"&gt;Reviews&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/#career" rel="noopener noreferrer"&gt;Career&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/offerings/" rel="noopener noreferrer"&gt;Products &amp;amp; Services&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/fun.html" rel="noopener noreferrer"&gt;Fun&lt;/a&gt;&lt;br&gt;
&lt;a href="https://marcindudek.dev/privacy/" rel="noopener noreferrer"&gt;Privacy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://github.com/MarcinDudekDev" rel="noopener noreferrer"&gt;https://github.com/MarcinDudekDev&lt;/a&gt;)&lt;br&gt;
[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://www.linkedin.com/in/marcin-dudek-4992a297/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/marcin-dudek-4992a297/&lt;/a&gt;)&lt;br&gt;
[&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://x.com/myththrazz" rel="noopener noreferrer"&gt;https://x.com/myththrazz&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;© 2026 Marcin Dudek · &lt;a href="https://we-got-this.biz/" rel="noopener noreferrer"&gt;We Got This sp. z o.o.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>debate</category>
    </item>
    <item>
      <title>288,493 Requests in 24 Hours — How I Spotted an XML-RPC Brute Force From a Weird Cache Ratio</title>
      <dc:creator>Marcin Dudek</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:58:08 +0000</pubDate>
      <link>https://dev.to/marcindudekdev/288493-requests-in-24-hours-how-i-spotted-an-xml-rpc-brute-force-from-a-weird-cache-ratio-1lah</link>
      <guid>https://dev.to/marcindudekdev/288493-requests-in-24-hours-how-i-spotted-an-xml-rpc-brute-force-from-a-weird-cache-ratio-1lah</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://marcindudek.dev/blog/xmlrpc-brute-force-cache-rate/" rel="noopener noreferrer"&gt;https://marcindudek.dev/blog/xmlrpc-brute-force-cache-rate/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TL;DR&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare cache hit ratio on a WordPress site dropped to 0.8% — the real alarm, not CPU or uptime.&lt;/li&gt;
&lt;li&gt;Root cause: a single Singapore DigitalOcean IP flooding &lt;code&gt;/xmlrpc.php&lt;/code&gt; with 288,493 POSTs in 24 hours, using &lt;code&gt;system.multicall&lt;/code&gt; to brute-force hundreds of credentials per request.&lt;/li&gt;
&lt;li&gt;Fix: Cloudflare WAF rule blocking &lt;code&gt;/xmlrpc.php&lt;/code&gt; at the edge, plus WP Multitool's Frontend Optimizer disabling xmlrpc inside WordPress — defense in depth, both layers on by default in 1.1.19.&lt;/li&gt;
&lt;li&gt;Action for you: check your Cloudflare Top Paths weekly. If &lt;code&gt;xmlrpc.php&lt;/code&gt; shows up in the top 3, you're already being hit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today I was glancing at Cloudflare analytics for one of my sites and something looked off. Cache hit ratio: &lt;strong&gt;0.8%&lt;/strong&gt;. That's not a typo. Zero point eight percent.&lt;/p&gt;

&lt;p&gt;For a mostly-static WordPress site that should be sitting at 70–90%, 0.8% means something is very wrong. Either the cache rules are broken, or something is flooding the site with uncacheable traffic. Turned out to be the second one.&lt;/p&gt;

&lt;p&gt;I pulled up the traffic breakdown and the answer was sitting right there. One IP from Singapore, &lt;strong&gt;288,493 requests in 24 hours&lt;/strong&gt;, all POSTs to &lt;code&gt;/xmlrpc.php&lt;/code&gt;, all returning 200. That's about 12,000 requests per hour from a single DigitalOcean droplet. The site was still up because Cloudflare was absorbing most of it, but my origin was burning CPU on every single one of those requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 0.8% Cache Rate Was the Real Signal
&lt;/h2&gt;

&lt;p&gt;Here's the thing about WordPress attacks on &lt;code&gt;xmlrpc.php&lt;/code&gt; — they're almost invisible if you only watch uptime. The site loads fine for real visitors. The CPU graph might look a bit elevated. But the attack itself doesn't trigger any obvious alarm.&lt;/p&gt;

&lt;p&gt;Cache rate is a great canary because &lt;code&gt;xmlrpc.php&lt;/code&gt; is POST-only and marked dynamic. Every attack request counts against your cache rate denominator. When you see 288k uncacheable dynamic requests vs a few thousand normal cached ones, the ratio collapses.&lt;/p&gt;

&lt;p&gt;So if your cache rate suddenly drops on an otherwise quiet WordPress site, don't immediately blame your plugins. Check what's being requested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What system.multicall Actually Does
&lt;/h2&gt;

&lt;p&gt;The vector is old but still works because so many sites leave xmlrpc enabled by default. Attackers POST XML payloads like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;  &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multicall&lt;/span&gt;



        &lt;span class="n"&gt;methodNamewp&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getUsersBlogs&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;
          &lt;span class="n"&gt;admin&lt;/span&gt;
          &lt;span class="n"&gt;password1&lt;/span&gt;


      &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hundreds&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="n"&gt;credential&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One HTTP request, hundreds of login attempts. &lt;strong&gt;That's the amplification.&lt;/strong&gt; A regular &lt;code&gt;wp-login.php&lt;/code&gt; brute force triggers rate limiting and WAF rules after a few attempts. &lt;code&gt;system.multicall&lt;/code&gt; lets you test 500 credentials in a single POST that looks like normal API traffic. That's why it's still the preferred vector in 2026 — efficient, quiet, and slips past naive rate limits that count requests instead of auth attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Check If You're Being Hit
&lt;/h2&gt;

&lt;p&gt;If you're on Cloudflare, the fastest way is the GraphQL analytics API. Query the top paths for the last 24 hours, grouped by path and cache status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$zone&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$since&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$until&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;viewer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;zones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zoneTag&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$zone&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;httpRequestsAdaptiveGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime_geq&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$since&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime_leq&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$until&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;orderBy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count_DESC&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;count&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;clientIP&lt;/span&gt; &lt;span class="n"&gt;clientCountryName&lt;/span&gt; &lt;span class="n"&gt;clientRequestPath&lt;/span&gt; &lt;span class="n"&gt;cacheStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;/xmlrpc.php&lt;/code&gt; shows up in the top 3 paths with &lt;code&gt;cacheStatus&lt;/code&gt; "dynamic" and a request count in the tens of thousands, you're being hit. Also check &lt;code&gt;wp-login.php&lt;/code&gt; while you're there — where there's xmlrpc abuse there's usually a coordinated login flood from other IPs.&lt;/p&gt;

&lt;p&gt;If you don't use the API, the Cloudflare dashboard has the same data under Analytics → Traffic. The Top Paths widget will give it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #1 — Block It at the Edge
&lt;/h2&gt;

&lt;p&gt;The fastest mitigation is a Cloudflare WAF custom rule. One line expression, block action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/xmlrpc.php"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stops the requests at Cloudflare's edge before they reach your origin. No CPU cost, no PHP execution, no bandwidth. After I deployed this rule the 288k/day flood stopped hitting my server entirely and the cache rate started recovering within the hour.&lt;/p&gt;

&lt;p&gt;If you have dozens of zones to protect, the Cloudflare API makes this a one-liner per zone. I ran it on all my own zones in about 10 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #2 — Disable It at the WordPress Level Too
&lt;/h2&gt;

&lt;p&gt;Edge blocks are great but I'm a defense-in-depth person. If someone misconfigures the WAF or bypasses Cloudflare, I want WordPress itself to refuse xmlrpc requests.&lt;/p&gt;

&lt;p&gt;You can do it with a few lines in &lt;code&gt;functions.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'xmlrpc_enabled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'__return_false'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'xmlrpc_methods'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$methods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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;Or if you'd rather not touch code, any decent WordPress optimizer plugin has this option. My own plugin &lt;a href="https://wpmultitool.com/" rel="noopener noreferrer"&gt;WP Multitool&lt;/a&gt; has it in the Frontend Optimizer module and it's on by default in the latest version — which is why I updated all my sites to 1.1.19 after spotting this attack. Install, activate, done — xmlrpc is disabled both ways.&lt;/p&gt;

&lt;p&gt;The key insight: don't rely on a single layer. Edge WAF rules can be misconfigured, origin can be exposed directly, and plugins can be deactivated. Stack them so no single failure leaves you open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do You Actually Need xmlrpc in 2026?
&lt;/h2&gt;

&lt;p&gt;Probably not. &lt;code&gt;xmlrpc.php&lt;/code&gt; was the WordPress API before REST existed. These days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The WordPress REST API handles everything xmlrpc used to do, better and with proper auth&lt;/li&gt;
&lt;li&gt;Remote publishing apps have mostly moved to REST&lt;/li&gt;
&lt;li&gt;Even WooCommerce and major page builders don't touch xmlrpc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one remaining legit use case is the Jetpack mobile app, which still uses xmlrpc under the hood for some features. If you don't use Jetpack mobile, you can kill xmlrpc without losing anything. If you do, there are better ways — restrict xmlrpc to Jetpack's IP ranges via WAF rule instead of blocking outright.&lt;/p&gt;

&lt;p&gt;In my case none of my sites need it, so I blocked it everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Going Forward
&lt;/h2&gt;

&lt;p&gt;A few habits I'm adding after this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch cache rate, not just uptime.&lt;/strong&gt; A 5% drop in hit ratio on a static-ish site deserves a look. A 50% drop means something is happening right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check top paths weekly.&lt;/strong&gt; Five seconds in the Cloudflare dashboard, tells you immediately if an attack is warming up. Most brute force attempts start small and ramp up over days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block &lt;code&gt;xmlrpc.php&lt;/code&gt; preemptively on new sites.&lt;/strong&gt; It's one WAF rule. There's no reason to wait until you see 288k requests before adding it.&lt;/p&gt;

&lt;p&gt;And if you're running WordPress anywhere without a CDN or WAF in front of it, fix that first. Cloudflare's free tier would have absorbed this entire attack without breaking a sweat.&lt;/p&gt;

&lt;p&gt;The cache rate on my site is back to normal now. But I'm genuinely curious how many WordPress owners are getting hit right now and have no idea because they only check if the homepage loads.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
    </item>
  </channel>
</rss>
