<?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: Iliya Oblakov</title>
    <description>The latest articles on DEV Community by Iliya Oblakov (@iliyaoblakov).</description>
    <link>https://dev.to/iliyaoblakov</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%2F1578658%2F7b3b5d15-9e52-4626-9c3c-4f29f2e30440.jpg</url>
      <title>DEV Community: Iliya Oblakov</title>
      <link>https://dev.to/iliyaoblakov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iliyaoblakov"/>
    <language>en</language>
    <item>
      <title>How to check whether ChatGPT and Google AI Overviews name your local business</title>
      <dc:creator>Iliya Oblakov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:55:27 +0000</pubDate>
      <link>https://dev.to/iliyaoblakov/how-to-check-whether-chatgpt-and-google-ai-overviews-name-your-local-business-38ib</link>
      <guid>https://dev.to/iliyaoblakov/how-to-check-whether-chatgpt-and-google-ai-overviews-name-your-local-business-38ib</guid>
      <description>&lt;p&gt;If you own or market a clinic, a law firm or a service company in a UK or US city, your next client may ask ChatGPT or Google for the best [your type of business] in that city and get a short list of names. If you have never checked whether your business is on that list, this post shows you how to check it yourself with a browser and a spreadsheet. Below is the method I use and what it showed on a real med spa on 23 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one question asked once tells you very little
&lt;/h2&gt;

&lt;p&gt;AI answers change. Ask an engine the same question twice and it can name different businesses. Engines also guess where you are from your connection when the question has no place in it. So use fixed questions with the place name in each one, ask them the same way every week and log every answer in the same columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: write 10 fixed buyer questions
&lt;/h2&gt;

&lt;p&gt;Write the questions a real client would type: 8 general ones and 2 that use your business name. Here is the template I use. Fill in the brackets once and then never change the wording, because a reworded question can't be compared with last week's answer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What are the best [type of business] in [city]?&lt;/li&gt;
&lt;li&gt;Where can I get [main service] in [neighbourhood], [city]?&lt;/li&gt;
&lt;li&gt;Which [city] [type of business] offer [service] packages?&lt;/li&gt;
&lt;li&gt;[Service] near [neighbourhood], [city]: which ones are good?&lt;/li&gt;
&lt;li&gt;Which [city] [type of business] have [something you offer, such as a monthly membership]?&lt;/li&gt;
&lt;li&gt;Where can I get [second service] in [city]?&lt;/li&gt;
&lt;li&gt;Best [service] for [problem] in [city]: which [type of business]?&lt;/li&gt;
&lt;li&gt;I want a top rated [type of business] in [neighbourhood], [city] for a first visit. What do you recommend?&lt;/li&gt;
&lt;li&gt;What is [business name] in [city] known for?&lt;/li&gt;
&lt;li&gt;What is the address, phone number and opening hours of [business name] in [city]?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Put the city in every question. In my med spa run, question 8 named two neighbourhoods and left the city out. Both ChatGPT and Google answered it for places with the same names in another state, so that answer said nothing about the business I was checking. My connection was in Bulgaria, which is one more reason the engine can't be left to guess the place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: run them the same way every week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Open a private browser window and stay logged out.&lt;/li&gt;
&lt;li&gt;Start a new chat for every question in ChatGPT, and never ask a follow-up. In my run ChatGPT searched the web on its own, without a toggle.&lt;/li&gt;
&lt;li&gt;In Google, search the question exactly as written and read the AI Overview at the top. If no Overview shows, write that down, because that is a result too. Stay out of AI Mode, since it is a different product.&lt;/li&gt;
&lt;li&gt;Paste each question from your list instead of retyping it, and save the full answer and a screenshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: log the same columns for every answer
&lt;/h2&gt;

&lt;p&gt;One row per answer. Here is the table, with three real rows from my med spa run. Names are left out.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Named?&lt;/th&gt;
&lt;th&gt;Site cited or linked?&lt;/th&gt;
&lt;th&gt;Position&lt;/th&gt;
&lt;th&gt;Named instead&lt;/th&gt;
&lt;th&gt;Facts right?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;23 Oct&lt;/td&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;3, laser hair removal packages&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;td&gt;5 others&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23 Oct&lt;/td&gt;
&lt;td&gt;Google AI Overviews&lt;/td&gt;
&lt;td&gt;2, Botox in a neighbourhood&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;4 others&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;first in the map pack under the Overview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23 Oct&lt;/td&gt;
&lt;td&gt;Google AI Overviews&lt;/td&gt;
&lt;td&gt;10, address, phone and hours&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;no, Saturday hours wrong&lt;/td&gt;
&lt;td&gt;hours match a directory site&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fill in "Facts right?" for questions 9 and 10 only. Check every fact the answer states against your own website and your Google Business Profile. Write the names in "Named instead" in the order the engine gave them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one run on a real med spa showed
&lt;/h2&gt;

&lt;p&gt;I ran this list for a med spa that is not a client, logged out, in ChatGPT and Google AI Overviews.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Of the 8 general questions, ChatGPT named the med spa in 3 and the Google Overview named it in 1. In one of the 3 ChatGPT answers it appeared only as a pin on the map, with no mention in the text.&lt;/li&gt;
&lt;li&gt;In one Google search the med spa was missing from the Overview but came first in the map pack under it. Log the map pack in your notes column, because it is a separate result from the Overview.&lt;/li&gt;
&lt;li&gt;Both engines named it for both questions asked by name and linked its website each time.&lt;/li&gt;
&lt;li&gt;ChatGPT got the address, phone and hours right. The Google Overview said Saturday was 11 AM to 4 PM. The med spa's homepage says Saturday is by appointment only, its own contact page showed a different Saturday time again, and the hours in the Overview matched a third-party directory listing.&lt;/li&gt;
&lt;li&gt;For "What are the best med spas in [city]?", the sources ChatGPT showed were three med spa directories, Yelp and Google. The business's own website was not among them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is one run. The plan for this sample is 3 runs on 3 different days, and I'd want all three before calling anything a pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with the log
&lt;/h2&gt;

&lt;p&gt;Start with the facts column. If an engine gets your hours wrong, compare the hours on your homepage, contact page, Google Business Profile and directory listings, and make them all match. Then check your listing on the directories the engines cite.&lt;/p&gt;

&lt;p&gt;Check your own site's code too. In &lt;a href="https://rayastats.com/en/blog/med-spa-websites-what-ai-engines-read?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=audit-3wk-2026-10&amp;amp;utm_content=city-check-how-to" rel="noopener noreferrer"&gt;a check of 50 med spa homepages&lt;/a&gt; in October, at least 27 gave no street address in their structured data and 11 got the business name wrong there. That study asked no AI engine anything, so it shows missing facts on sites and says nothing about who gets named.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the full audit
&lt;/h2&gt;

&lt;p&gt;If you run a clinic, a law firm or a service company in a UK or US city and want this done properly, the &lt;a href="https://rayastats.com/en/ai-visibility-audit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=audit-3wk-2026-10&amp;amp;utm_content=city-check-how-to" rel="noopener noreferrer"&gt;AI Visibility Audit&lt;/a&gt; is the same method run in depth. I write your city into every prompt and run each prompt 3 times in each engine in your package, on 3 different days, with a fresh browser session and a new chat each time. You get screenshots of what the engines answered and which businesses they named instead of you, a check of your Google Business Profile and how you show in the map pack, your reviews compared with 3 local competitors, a technical check of up to 50 pages of your site and a list of the questions clients ask that your site never answers. All of that becomes a prioritized list of fixes, and each fix says who should do it. The report arrives 3 to 5 business days after you answer the intake email, and if it is late or has fewer than 10 specific, prioritized fixes, you get a full refund and keep the report. The audit is read-only, so I never ask for passwords or access to your accounts.&lt;/p&gt;

&lt;p&gt;If you want a quick first look before that, the &lt;a href="https://rayastats.com/en/ai-visibility-audit/free-city-check?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=audit-3wk-2026-10&amp;amp;utm_content=city-check-how-to" rel="noopener noreferrer"&gt;free AI city check&lt;/a&gt; asks ChatGPT and Google AI Overviews 3 client questions with your city in each, once in each engine, and I email you who they named.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>chatgpt</category>
      <category>smallbusiness</category>
    </item>
    <item>
      <title>Why my review gate refuses to run on a dirty working tree</title>
      <dc:creator>Iliya Oblakov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 06:13:43 +0000</pubDate>
      <link>https://dev.to/iliyaoblakov/why-my-review-gate-refuses-to-run-on-a-dirty-working-tree-56pn</link>
      <guid>https://dev.to/iliyaoblakov/why-my-review-gate-refuses-to-run-on-a-dirty-working-tree-56pn</guid>
      <description>&lt;p&gt;Under my last post on dev.to, a reader asked whether merge-check also refuses when the working tree has uncommitted changes. It does, and so do the review and test scripts. This post covers what counts as dirty, where the check runs and why it is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup in one paragraph
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/IliyaOblakov/two-engines-gate" rel="noopener noreferrer"&gt;two-engines-gate&lt;/a&gt; is the free, MIT licensed version of how I make Claude Code and Codex check each other. It has three scripts. &lt;code&gt;gate.sh&lt;/code&gt; runs your tests and writes a pass record named after the commit. &lt;code&gt;review.sh&lt;/code&gt; sends the commit to the other engine for a read-only review and saves the verdict under the commit hash. &lt;code&gt;merge-check.sh&lt;/code&gt; refuses the merge unless the commit you are on has both. I wrote about &lt;a href="https://rayastats.com/en/blog/how-to-make-codex-review-claude-code-commits?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=kit-3wk-2026-10&amp;amp;utm_content=dirty-tree-refusal" rel="noopener noreferrer"&gt;the review loop&lt;/a&gt; and &lt;a href="https://rayastats.com/en/blog/review-verdict-saved-under-commit-hash?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=kit-3wk-2026-10&amp;amp;utm_content=dirty-tree-refusal" rel="noopener noreferrer"&gt;the commit hash records&lt;/a&gt; before. This post is about the one check all three scripts share.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check itself
&lt;/h2&gt;

&lt;p&gt;Each script defines the same small function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clean&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c"&gt;# fails on changed or untracked files outside .ai/, and when git status itself fails&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;out&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt; &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;':(exclude).ai'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git status --porcelain&lt;/code&gt; prints one line per changed file in a format meant for scripts. &lt;code&gt;--untracked-files=all&lt;/code&gt; lists every untracked file, including files inside new folders. The pathspec &lt;code&gt;':(exclude).ai'&lt;/code&gt; leaves out the &lt;code&gt;.ai/&lt;/code&gt; folder, which is where the gate writes its verdicts, pass records and logs. If the command prints anything, the tree is dirty. If git status itself fails, the function fails too, so a broken repository never looks clean by accident. That is why the refusal message says "or git status failed".&lt;/p&gt;

&lt;p&gt;I ran each case against the public repo on 23 October to see exactly what the scripts do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State of the checkout&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A tracked file edited and not committed&lt;/td&gt;
&lt;td&gt;refused by all three scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A change staged with &lt;code&gt;git add&lt;/code&gt; but not committed&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A new file that git does not track&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An edited &lt;code&gt;.gitignore&lt;/code&gt; that is not committed&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A new file under &lt;code&gt;.ai/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A new file inside a folder that &lt;code&gt;.gitignore&lt;/code&gt; covers&lt;/td&gt;
&lt;td&gt;allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The refusal looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;review: REFUSED: uncommitted changes or untracked files (or git status failed); commit or remove them first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the &lt;code&gt;.gitignore&lt;/code&gt; row. The README asks you to add &lt;code&gt;.ai/&lt;/code&gt; to the project's &lt;code&gt;.gitignore&lt;/code&gt;, and that edit is itself an uncommitted change until you commit it, so the first review gets refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the review needs a clean tree
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;review.sh&lt;/code&gt; builds the reviewer's prompt from the commit log and &lt;code&gt;git diff base...HEAD&lt;/code&gt;. Both of those only contain committed work. The verdict is then saved under the hash of HEAD.&lt;/p&gt;

&lt;p&gt;The review prompt also tells the reviewer it may read any file in the checkout for context, and Codex in its read-only sandbox can do that. So if an edit sits in the working tree without a commit, the reviewer can open a file whose contents differ from the commit it is judging. Its ACCEPT would then be saved under a hash that does not contain the code it looked at. When the script refuses before the review starts, the files on disk match the commit under review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the test run needs a clean tree
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gate.sh&lt;/code&gt; runs your test command on the files on disk, and the files on disk are what the test command sees. An untracked helper file or a local fix that never got committed can make the tests pass. The pass record would then carry the hash of a commit that might fail on a fresh checkout. The guide in my kit puts it in one line: the gate refuses a dirty tree because "a check could pass on a file that is not in the commit".&lt;/p&gt;

&lt;h2&gt;
  
  
  The check runs before and after
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;review.sh&lt;/code&gt; and &lt;code&gt;gate.sh&lt;/code&gt; run the check twice.&lt;/p&gt;

&lt;p&gt;Before anything starts, each script deletes the earlier evidence for this commit. &lt;code&gt;review.sh&lt;/code&gt; removes the old verdict file and &lt;code&gt;gate.sh&lt;/code&gt; removes the old pass record. Only then does the clean check run. So a refused run cannot leave an old ACCEPT or an old pass sitting there for this commit.&lt;/p&gt;

&lt;p&gt;After the reviewer or the tests finish, the script checks two things again: that HEAD has not moved, and that the tree is still clean. If the reviewer changed a file, &lt;code&gt;review.sh&lt;/code&gt; prints &lt;code&gt;REVIEW VOID: files changed during the review&lt;/code&gt; and saves no verdict. If the test command left a file behind, &lt;code&gt;gate.sh&lt;/code&gt; fails the run. I tested that with a test command that creates a stray file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gate: running 'bash test.sh; touch stray.txt' on 986def16acb8
gate: the run left changed or untracked files (or git status failed)
gate: FAIL (exit 1) on 986def16acb8; no pass record; full log .ai/tests/986def16acb8.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test suite that writes snapshots or build output into the repo will hit this. The way out is to make the suite write somewhere git ignores, and to commit that &lt;code&gt;.gitignore&lt;/code&gt; change.&lt;/p&gt;

&lt;h2&gt;
  
  
  merge-check runs it first
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;merge-check.sh&lt;/code&gt; runs the same git status check before it looks at any pass record or verdict. The merge only takes the commit, so uncommitted edits would not be merged anyway. They would still mean the checkout holds work nobody reviewed or tested. With a clean checkout, a yes from merge-check covers exactly what is on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check does not cover
&lt;/h2&gt;

&lt;p&gt;Know these limits before you rely on the check.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ignored files are invisible to it. &lt;code&gt;git status&lt;/code&gt; does not list files that &lt;code&gt;.gitignore&lt;/code&gt; covers, so a test that reads an ignored config file or a build cache can still behave differently on a fresh checkout. The gate does not catch that.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.ai/&lt;/code&gt; is excluded on purpose, which means anything you put there is outside the check. The README calls the gate "a local guard against merging the wrong commit, not a security boundary", because anything with write access to the repository can write files under &lt;code&gt;.ai/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It only checks the local checkout. It does not push, merge or look at what is on the remote.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The same rule in the kit
&lt;/h2&gt;

&lt;p&gt;The paid &lt;a href="https://rayastats.com/en/two-engines-kit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=kit-3wk-2026-10&amp;amp;utm_content=dirty-tree-refusal" rel="noopener noreferrer"&gt;Two Engines Kit&lt;/a&gt; (currently v1.0.2) has the same rule in its own scripts. Its &lt;code&gt;cross-review.sh&lt;/code&gt; and &lt;code&gt;gate.sh&lt;/code&gt; both refuse a worktree with uncommitted changes or untracked files, and the guide says: "A dirty tree or an untracked file gets no record, so you cannot gate code that is not in the commit and then merge the commit." The Two Engines Kit is built on information gathered since the first Codex and Claude CLIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Clone &lt;a href="https://github.com/IliyaOblakov/two-engines-gate" rel="noopener noreferrer"&gt;two-engines-gate&lt;/a&gt; and run &lt;code&gt;bash demo.sh&lt;/code&gt; to watch one full loop with a stand-in reviewer, so no model or account is used. Then edit a file in a project that uses the gate, leave it uncommitted, and run &lt;code&gt;review.sh&lt;/code&gt; to see the refusal. The test suite in &lt;code&gt;tests/run.sh&lt;/code&gt; has a dirty tree case and a "files changed during review" case if you want to read the check from the test side.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>codex</category>
      <category>git</category>
      <category>codereview</category>
    </item>
    <item>
      <title>Why my review gate saves each verdict under the exact commit hash</title>
      <dc:creator>Iliya Oblakov</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:59:21 +0000</pubDate>
      <link>https://dev.to/iliyaoblakov/why-my-review-gate-saves-each-verdict-under-the-exact-commit-hash-3d11</link>
      <guid>https://dev.to/iliyaoblakov/why-my-review-gate-saves-each-verdict-under-the-exact-commit-hash-3d11</guid>
      <description>&lt;p&gt;Two recent items in the Claude Code repository, a changelog fix and a bug report, describe two ways a review step can go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Anthropic fixed in Claude Code 2.1.305
&lt;/h2&gt;

&lt;p&gt;The Claude Code changelog for version 2.1.305 has this line about Code Review, Anthropic's managed pull request reviewer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"[Code Review] Fixed Code Review sometimes not reviewing the newest commit when several pushes landed on a pull request during a review"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;Claude Code CHANGELOG.md, version 2.1.305&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If nothing records which commit a review actually read, a review of an older commit looks the same as a review of the newest one. Anthropic has fixed this in its own product. The same risk exists in any setup where new commits can land while a review is running, a local one included.&lt;/p&gt;

&lt;h2&gt;
  
  
  What issue #103536 reports
&lt;/h2&gt;

&lt;p&gt;Issue #103536 was opened on 19 Oct 2026 at 20:45 Sofia time and is titled "[BUG] Model edits tracked files during a read-only review, despite loaded 'never edit unless asked / zero autonomy' rules". The reporter writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"While re-reviewing pull requests, the model edited a tracked file (&lt;code&gt;CHANGELOG.md&lt;/code&gt;) on its own initiative. It removed a duplicated entry and reworded another, and the user had not asked for either."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The environment section says why nothing stopped it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Auto-approval allowed the Edit and Bash tools, so nothing stopped the write"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/anthropics/claude-code/issues/103536" rel="noopener noreferrer"&gt;anthropics/claude-code issue #103536&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;According to the issue, the rules telling the model not to edit were loaded every turn from CLAUDE.md and MEMORY.md. The Edit tool was still available, so the edit went through.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the gate ties a verdict to one commit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/IliyaOblakov/two-engines-gate" rel="noopener noreferrer"&gt;two-engines-gate&lt;/a&gt; is a free, MIT-licensed local review and merge gate for Claude Code and Codex.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bin/review.sh&lt;/code&gt; reads the full hash of HEAD at the start and saves the verdict as &lt;code&gt;.ai/reviews/&amp;lt;first 12 characters of HEAD&amp;gt;.md&lt;/code&gt; (lines 22 and 23). It deletes any earlier verdict for that commit first (line 24). After the reviewer exits, it checks that HEAD is still the same commit and stops with "REVIEW VOID: HEAD moved during the review" if it has changed (line 74). The saved file starts with a &lt;code&gt;Head:&lt;/code&gt; line that holds the full 40-character hash (line 82).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bin/merge-check.sh&lt;/code&gt; then looks for &lt;code&gt;.ai/reviews/&amp;lt;first 12 characters of HEAD&amp;gt;.md&lt;/code&gt; and refuses if the file is missing or if its &lt;code&gt;Head:&lt;/code&gt; line names a different commit (lines 21 to 23). It also needs a test pass record named after the full hash, &lt;code&gt;.ai/tests/&amp;lt;HEAD&amp;gt;.pass&lt;/code&gt; (lines 18 to 20), and a verdict whose reviewer is a different engine from the builder (lines 24 to 27).&lt;/p&gt;

&lt;p&gt;When a new commit lands after a review, merge-check finds no verdict for it and refuses. The README gives the reason in two sentences: "an ACCEPT for an older commit can quietly cover code nobody reviewed. A new commit needs a new review and a new test run."&lt;/p&gt;

&lt;h2&gt;
  
  
  How the reviewer is kept away from Edit and Write
&lt;/h2&gt;

&lt;p&gt;The gate's reviewer is always the engine that didn't write the code. With the default &lt;code&gt;BUILDER=claude&lt;/code&gt;, Codex reviews, and review.sh calls it as &lt;code&gt;codex exec -s read-only&lt;/code&gt; (bin/review.sh, line 60). docs/engines.md (line 13) describes this as the read-only sandbox, where Codex can read files and run read-only commands such as &lt;code&gt;git diff&lt;/code&gt; but cannot write.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;BUILDER=codex&lt;/code&gt;, Claude Code reviews. This is the call in bin/review.sh (lines 66 to 68), shown here without the optional model argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Do the review described on standard input."&lt;/span&gt; &lt;span class="nt"&gt;--restricted&lt;/span&gt; &lt;span class="nt"&gt;--strict-mcp-config&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; dontAsk &lt;span class="nt"&gt;--tools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob"&lt;/span&gt; &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disallowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Bash,Edit,MultiEdit,Write,NotebookEdit,WebFetch,WebSearch,Task,Agent"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--tools "Read,Grep,Glob"&lt;/code&gt; limits the session to those three tools, and &lt;code&gt;--disallowedTools&lt;/code&gt; also names Edit, MultiEdit, Write, NotebookEdit and Bash. docs/engines.md (line 30) says that with this call Claude Code "cannot run commands, git included, and cannot edit or write files", which is why review.sh puts the commits and the full diff into the prompt.&lt;/p&gt;

&lt;p&gt;review-prompt.md also tells the reviewer "do not edit, create or delete files", and the gate doesn't depend on that sentence being obeyed. In issue #103536 the edit went through because the Edit tool was allowed, and in the gate's Claude Code call the reviewer starts without it. As a second check, review.sh runs &lt;code&gt;git status&lt;/code&gt; after the review and stops with "REVIEW VOID: files changed during the review" if any file outside &lt;code&gt;.ai/&lt;/code&gt; changed (line 75).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gate doesn't do
&lt;/h2&gt;

&lt;p&gt;The README lists its limits. It says the gate "does not make the code correct" and describes it as "a local guard against merging the wrong commit, not a security boundary", because anything with write access to the repository can write files under &lt;code&gt;.ai/&lt;/code&gt;. It doesn't push, merge or talk to GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and the kit
&lt;/h2&gt;

&lt;p&gt;The free gate is at &lt;a href="https://github.com/IliyaOblakov/two-engines-gate" rel="noopener noreferrer"&gt;github.com/IliyaOblakov/two-engines-gate&lt;/a&gt;. &lt;code&gt;bash demo.sh&lt;/code&gt; in a clone walks one change through it with a stand-in reviewer, so it needs no model, account or network.&lt;/p&gt;

&lt;p&gt;The Two Engines Kit is the paid kit, currently v1.0.2. In it Claude Code writes the code, Codex reviews it read-only, the review script saves the verdict under the commit Codex reviewed, and the merge script refuses to touch GitHub without a passing test run on that same commit. With &lt;code&gt;REQUIRE_REVIEW=1&lt;/code&gt; it also refuses a commit that has no saved ACCEPT. The Two Engines Kit is built on information gathered since the first Codex and Claude CLIs. It costs $29 for the first 30 sales, then $39, at &lt;a href="https://rayastats.com/en/two-engines-kit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=kit-3wk-2026-10&amp;amp;utm_content=commit-hash-verdict" rel="noopener noreferrer"&gt;rayastats.com/en/two-engines-kit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>codereview</category>
      <category>git</category>
      <category>codex</category>
    </item>
    <item>
      <title>What 50 med spa websites tell AI engines about themselves</title>
      <dc:creator>Iliya Oblakov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 22:41:02 +0000</pubDate>
      <link>https://dev.to/iliyaoblakov/what-50-med-spa-websites-tell-ai-engines-about-themselves-2923</link>
      <guid>https://dev.to/iliyaoblakov/what-50-med-spa-websites-tell-ai-engines-about-themselves-2923</guid>
      <description>&lt;p&gt;On 21 and 22 October 2026 I checked the homepages of 50 small med spas in Texas, leaving out national chains and franchises. For each site I fetched the homepage and read the parts that software reads and visitors usually don't, such as the page title, the meta description, the main heading and the structured data. Structured data is a block of code that can give search engines and AI tools the business name, address, phone number and opening hours in a fixed format. On some sites I also looked at the sitemap and robots.txt.&lt;/p&gt;

&lt;p&gt;For this article I did not ask ChatGPT, Perplexity or Google AI Overviews about any of these businesses. Everything below describes what the website code says, which affects how much clear, consistent information the site gives a tool that tries to describe the business. A problem in the code does not prove that an AI engine leaves a business out of its answers, and a clean site does not guarantee that it gets named.&lt;/p&gt;

&lt;p&gt;These 50 are also not a random sample. I was looking for sites with something worth fixing, and I set aside at least 14 other sites that looked fine. The counts below are the problems I noted on each site, so the real numbers on these 50 sites may be higher. Please read them as what came up in this check, since they can't tell you how common these problems are across the industry.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than half had no address in the code
&lt;/h2&gt;

&lt;p&gt;At least 27 of the 50 homepages gave no street address in their structured data. Seven had no structured data at all. On the other 20 the structured data was there, but the address field was missing or empty. On at least 5 of those 20 the street address was printed on the page, so only the code was missing it.&lt;/p&gt;

&lt;p&gt;On those sites a tool that relies on the structured data gets a name and maybe a phone number, and has to work out the location from loose page text.&lt;/p&gt;

&lt;p&gt;Two more sites had a full address in the code that differed from the address shown on the page. On one of them the code pointed to a street in a different town. From the outside I can't tell whether that came from a copied template or an old location that nobody updated.&lt;/p&gt;

&lt;p&gt;At least 5 sites showed two or more locations on the page and gave no address for any of them in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The business name in the code was often wrong
&lt;/h2&gt;

&lt;p&gt;The name is the most basic fact in structured data, and 11 of the 50 sites got it wrong in some way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 had the name misspelled.&lt;/li&gt;
&lt;li&gt;2 had a blank name, or no name field at all.&lt;/li&gt;
&lt;li&gt;1 used two people's names instead of the business name.&lt;/li&gt;
&lt;li&gt;1 used the words "Medical Spa" as the name, with nothing else.&lt;/li&gt;
&lt;li&gt;1 still had what looks like a working name from when the site was built.&lt;/li&gt;
&lt;li&gt;1 had a stray number added to the end of the name.&lt;/li&gt;
&lt;li&gt;1 used a different name in the code from the one in the page title.&lt;/li&gt;
&lt;li&gt;2 had page text attached to the name, in one case the words "Privacy Policy" and in the other the whole homepage headline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two sites also labelled the business in the code as both a person and an organisation, and one credited a founder in the code who was not the owner named on the About page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page titles that say "Home"
&lt;/h2&gt;

&lt;p&gt;The title tag is the text in the browser tab, and Google often uses it as the blue link in its results. On 6 of the 50 sites it started with the word "Home", followed by the business name or the site's web address, and none of those 6 named the town or a treatment. Counting those 6, at least 10 of the titles I read named neither the town nor a treatment. One title had the treatments and the town but no business name, and another left out both the name and the town.&lt;/p&gt;

&lt;p&gt;A title like "Botox and fillers in [town] | [business name]" tells a search engine in one line what the page is about and where the business is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Missing descriptions, empty headings and thin pages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;9 sites had no meta description, or an empty one. That is the short summary search engines often show under the title.&lt;/li&gt;
&lt;li&gt;10 sites had no main heading (H1) on the homepage, or an empty one. Two more had "Home" as the first main heading, on one the first two main headings were just the names of two towns, and one had dozens of H1 tags on the same page, several containing nothing but a "|" character.&lt;/li&gt;
&lt;li&gt;4 homepages had fewer than 300 words of visible text. The shortest had about 145 words, which leaves very little for any tool to quote or summarise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Placeholder pages that went live
&lt;/h2&gt;

&lt;p&gt;Two sites were publishing template pages. One had eight live pages titled "Service title", "Service title 2" and so on up to 8, all listed in the sitemap and all loading normally. The other listed placeholder blog post addresses in its sitemap and had a live page called "New Page". Search engines can find pages like these through the sitemap and index them like any other page on the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not count as a problem
&lt;/h2&gt;

&lt;p&gt;Many of these sites have no llms.txt file, and I did not count that against them. Google's own guidance says its AI features need no special schema, and that llms.txt can be ignored for Google Search.&lt;/p&gt;

&lt;p&gt;I also looked at robots.txt on several sites to see whether they block AI crawlers. On the Squarespace sites I checked, the AI bots are listed but grouped under the general rule, so they are not blocked. Of the sites where I read robots.txt, only one blocked an AI-related crawler (Amazonbot).&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can check on your own site
&lt;/h2&gt;

&lt;p&gt;None of these fixes needs a new website, and most are small edits for whoever looks after the site. You can check the basics yourself in a few minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your homepage, right click, choose "View page source" and search for "ld+json". If nothing comes up, the page has no structured data of the kind I looked at.&lt;/li&gt;
&lt;li&gt;If it is there, check that the name, street address, phone number and opening hours match what your site and your Google Business Profile show, word for word.&lt;/li&gt;
&lt;li&gt;Look at the text in your browser tab. If it says "Home", change it to your main treatments, your town and your business name.&lt;/li&gt;
&lt;li&gt;Open yoursite.com/sitemap.xml and look for pages you never meant to publish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote a longer walkthrough of free checks like these in &lt;a href="https://rayastats.com/en/blog/can-ai-engines-read-your-website" rel="noopener noreferrer"&gt;Can AI engines read your website? A 15-minute check&lt;/a&gt;, and how AI engines choose which businesses to recommend is covered in &lt;a href="https://rayastats.com/en/blog/geo-for-small-businesses" rel="noopener noreferrer"&gt;GEO for small businesses&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want this done for your business
&lt;/h2&gt;

&lt;p&gt;The checks above cover only the website code. In my AI Visibility Audit I also run live questions about your kind of business in AI engines, record which businesses get named and how yours is described, and give you a prioritised list of fixes.&lt;/p&gt;

&lt;p&gt;You can see what is included, the packages and the turnaround on the audit page: &lt;a href="https://rayastats.com/en/ai-visibility-audit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=audit-3wk-2026-10&amp;amp;utm_content=medspa-50-sites" rel="noopener noreferrer"&gt;AI Visibility Audit for med spas and local businesses&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>smallbusiness</category>
    </item>
    <item>
      <title>How to make Codex review Claude Code's commits: done means, house rules and round rules</title>
      <dc:creator>Iliya Oblakov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 15:04:18 +0000</pubDate>
      <link>https://dev.to/iliyaoblakov/how-to-make-codex-review-claude-codes-commits-done-means-house-rules-and-round-rules-2ili</link>
      <guid>https://dev.to/iliyaoblakov/how-to-make-codex-review-claude-codes-commits-done-means-house-rules-and-round-rules-2ili</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/iliyaoblakov/how-i-make-claude-code-and-codex-review-each-others-work-5dmh"&gt;my last post&lt;/a&gt; I showed the basic loop: Claude Code writes in its own worktree, Codex reviews the commit read-only, the tests run on that same commit, and a script refuses the merge unless both pieces of evidence exist.&lt;/p&gt;

&lt;p&gt;That loop has a weak spot. A reviewer that doesn't know what "done" means reviews against its own taste. It flags style, misses the one requirement that mattered, and finds something new every round. The loop never ends, or it ends because you got tired.&lt;/p&gt;

&lt;p&gt;This post fixes that with three things: a definition of done the reviewer checks line by line, house rules it checks the same way, and round rules so the loop stops. I've been building this since the first Codex and Claude CLIs, and these three go into every review on my own projects.&lt;/p&gt;

&lt;p&gt;A review without a finish line is an opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup in one paragraph
&lt;/h2&gt;

&lt;p&gt;Claude Code writes. It commits each change on a branch and stops. Codex reviews that commit in a read-only sandbox: it can read files and run read-only commands like &lt;code&gt;git diff&lt;/code&gt;, but it can't write anything. The prompt tells it never to hand back a patch, so every fix goes back to Claude Code as a new commit, and the new commit gets a new review. The verdict is one line, &lt;code&gt;Verdict: ACCEPT&lt;/code&gt; or &lt;code&gt;Verdict: CHANGES REQUIRED&lt;/code&gt;, saved in a file named after the commit it reviewed.&lt;/p&gt;

&lt;p&gt;If you want that part ready to run, the free gate is at &lt;a href="https://github.com/IliyaOblakov/two-engines-gate" rel="noopener noreferrer"&gt;github.com/IliyaOblakov/two-engines-gate&lt;/a&gt; (MIT). Everything below sits on top of it, or on top of your own scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: write the brief before the code
&lt;/h2&gt;

&lt;p&gt;Most review noise starts before any code exists. If the only instruction Claude Code got was "add CSV export", the reviewer has nothing to check the result against except its own opinion.&lt;/p&gt;

&lt;p&gt;So every task starts with a short brief. Mine has four parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Brief: CSV export for the orders page&lt;/span&gt;

&lt;span class="gu"&gt;## Done means&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; A "Download CSV" button on /orders downloads the orders currently
   shown, with the current filters applied.
&lt;span class="p"&gt;2.&lt;/span&gt; Columns, in order: id, created_at (ISO 8601, UTC), customer_email,
   total (two decimals, no currency sign), status.
&lt;span class="p"&gt;3.&lt;/span&gt; A customer email that contains a comma or a quote is escaped, so
   the file opens in a spreadsheet with the right columns.
&lt;span class="p"&gt;4.&lt;/span&gt; A new test covers point 3 and fails on the code before this change.

&lt;span class="gu"&gt;## Must stay exactly as it is&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; The JSON response of GET /api/orders.
&lt;span class="p"&gt;-&lt;/span&gt; Existing filters and their URL parameters.

&lt;span class="gu"&gt;## How it is checked&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Point 1: the route and the button in the diff.
&lt;span class="p"&gt;-&lt;/span&gt; Points 2 and 3: the new test.
&lt;span class="p"&gt;-&lt;/span&gt; Point 4: the test run once against the base commit, output pasted
  in the commit body.

&lt;span class="gu"&gt;## Out of scope&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Exports for other pages. Scheduled exports.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line under "Done means" has to be something a reviewer can confirm from the diff or from a test. "Export works well" isn't a line. "A customer email with a comma is escaped" is.&lt;/p&gt;

&lt;p&gt;Point 4 is worth stealing on its own. The engine that writes the code often writes the tests too, and a test that already passes on the old code proves nothing about the change. Ask the writer to run each new test once against the base commit and show that it failed there.&lt;/p&gt;

&lt;p&gt;Where the brief lives matters. Commit it with the change, or keep it under &lt;code&gt;.ai/&lt;/code&gt;. The free gate refuses to review a checkout with uncommitted changes or untracked files anywhere else, and it skips &lt;code&gt;.ai/&lt;/code&gt; for exactly this kind of working file. If you add &lt;code&gt;.ai/&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt;, commit that change too, or the edited &lt;code&gt;.gitignore&lt;/code&gt; itself gets the review refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: house rules the reviewer can grep for
&lt;/h2&gt;

&lt;p&gt;The brief covers this task. House rules cover every task: how errors are handled, where config lives, what never goes into a log. The things you'd otherwise repeat in every review.&lt;/p&gt;

&lt;p&gt;Write them like the brief, as things a reviewer can find in a diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## House rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; No new dependency without a line in the commit body saying why.
&lt;span class="p"&gt;-&lt;/span&gt; Config comes from environment variables read in config.ts only.
  No process.env anywhere else.
&lt;span class="p"&gt;-&lt;/span&gt; Never log a full email address, token or request body.
&lt;span class="p"&gt;-&lt;/span&gt; User-facing strings go through t(), not hard-coded text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put them in the files both engines already read: CLAUDE.md for Claude Code, AGENTS.md for Codex. The writer follows them while writing, and the reviewer checks against the same text. Keep the AGENTS.md section short: Codex stops adding instruction files once their combined size reaches &lt;code&gt;project_doc_max_bytes&lt;/code&gt;, 32 KiB by default.&lt;/p&gt;

&lt;p&gt;Write the rule once. Check it every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: make the reviewer answer every line
&lt;/h2&gt;

&lt;p&gt;Now change what you ask for. Instead of "review this diff", ask for an answer per line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;The brief for this change is in .ai/brief.md.

For each line under "Done means" and each house rule, answer one of:
&lt;span class="p"&gt;-&lt;/span&gt; met: give the file:line in the diff that shows it
&lt;span class="p"&gt;-&lt;/span&gt; not met: give the file:line and what is missing
&lt;span class="p"&gt;-&lt;/span&gt; not checkable: say what evidence would be needed

Treat the commit message and the writer's summary as claims to verify,
not as evidence. If a section is empty, say "not given" instead of
guessing what it should have said.

Findings outside the brief and the house rules need a concrete failure:
the input or steps, and what goes wrong. Style preferences are not
findings.

Keep the verdict rule: exactly one line, Verdict: ACCEPT or
Verdict: CHANGES REQUIRED. ACCEPT only when every line above is met.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the free gate, add this to &lt;code&gt;review-prompt.md&lt;/code&gt;, which exists to be edited with your project rules. With your own scripts, append it to whatever prompt you send Codex.&lt;/p&gt;

&lt;p&gt;The reviewer now checks a list instead of free-styling. And a "not checkable" answer is a finding about your brief: you wrote a requirement nobody can verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: cap the rounds
&lt;/h2&gt;

&lt;p&gt;Without round rules, a reviewer at high effort can find a new nit every round, and the change never lands. So each round gets one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round 1&lt;/strong&gt; lists every finding. Nothing held back for later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round 2&lt;/strong&gt; confirms the fixes and may only add what the fixes broke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round 3 and later:&lt;/strong&gt; only blockers and security findings hold the merge. A blocker needs a failure you can reproduce. Everything else is a nit for a follow-ups file, and the reviewer can accept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At the cap:&lt;/strong&gt; if the last allowed round still says CHANGES REQUIRED, stop sending fixes. The verdict stays CHANGES REQUIRED, so the merge stays blocked, and a person decides: change the approach, split the change, or allow one more round on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the rules to the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Round rules. Round 1: list every finding. Round 2: confirm the fixes and
add only what the fixes broke. Round 3 and later: only blockers and
security findings may hold the merge; mark everything else as a nit for
the follow-ups file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reviewer also needs to know which round it's in. Count rounds in a small file per change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# rounds.sh &amp;lt;change-name&amp;gt;: count review rounds for one change; exit 3 at the cap&lt;/span&gt;
&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?usage:&lt;span class="p"&gt; rounds.sh &amp;lt;change-name&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MAX_ROUNDS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$max&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;[!&lt;/span&gt;0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MAX_ROUNDS must be a whole number"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--show-toplevel&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".ai/reviews/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.rounds"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .ai/reviews
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;n &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$max&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"last round reached for &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;; a person decides"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;3
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12 HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"round &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="nv"&gt;$max&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then feed the count into the prompt. With the free gate, a short wrapper does it. Save it as &lt;code&gt;review-round.sh&lt;/code&gt; in the root of your repository, in the same folder as &lt;code&gt;rounds.sh&lt;/code&gt;, and run it from that root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# review-round.sh &amp;lt;change-name&amp;gt;: one counted review round with the free gate&lt;/span&gt;
&lt;span class="nv"&gt;round&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bash rounds.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;path/to/two-engines-gate/review-prompt.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\nThis is %s.\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$round&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .ai/prompt.md
&lt;span class="nv"&gt;REVIEW_PROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.ai/prompt.md path/to/two-engines-gate/bin/review.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit both scripts, or list them in &lt;code&gt;.gitignore&lt;/code&gt; and commit that change, so the gate does not refuse them. The &lt;code&gt;.rounds&lt;/code&gt; file and &lt;code&gt;.ai/prompt.md&lt;/code&gt; they write sit under &lt;code&gt;.ai/&lt;/code&gt;, which the gate skips.&lt;/p&gt;

&lt;p&gt;A review that fails without any verdict still uses up a round. Delete the last line of the &lt;code&gt;.rounds&lt;/code&gt; file before you rerun it.&lt;/p&gt;

&lt;p&gt;The cap never turns into an ACCEPT. It only stops the loop and hands the decision to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: when Claude Code and Codex disagree
&lt;/h2&gt;

&lt;p&gt;Sometimes the writer pushes back on a finding, and sometimes it's right. Don't let the two engines argue it out over five rounds. One rule: a finding stands if it comes with a reproducible failure, and goes to follow-ups if it doesn't. Anything the brief doesn't settle, like scope or anything a user would see, is your call. That's one short question to you, not another round.&lt;/p&gt;

&lt;p&gt;Evidence wins. Opinions wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't do
&lt;/h2&gt;

&lt;p&gt;None of this makes code correct. A reviewer can mark a line "met" and be wrong, and tests only check what they test. What you get is a review that checks the things you said mattered, and a loop that ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/IliyaOblakov/two-engines-gate" rel="noopener noreferrer"&gt;free gate&lt;/a&gt; has the read-only review, the verdict named after the commit, the test pass record, and a merge check that refuses without both, plus an offline demo that needs neither CLI. It runs in both directions: &lt;code&gt;BUILDER=codex&lt;/code&gt; makes Claude Code the reviewer. Add the "Done means" section, the house rules and the round rules to its &lt;code&gt;review-prompt.md&lt;/code&gt;, put &lt;code&gt;rounds.sh&lt;/code&gt; and the wrapper in your repository root, and you have everything in this post.&lt;/p&gt;

&lt;p&gt;If you want the full workflow I use, the Two Engines Kit is a paid kit I made. It adds one rulebook both engines load, hooks and settings for both engines, a GitHub merge script that merges exactly the commit that was checked, and an installer that previews every change and can be undone byte for byte. $29 for the first 30 sales, then $39: &lt;a href="https://rayastats.com/en/two-engines-kit" rel="noopener noreferrer"&gt;https://rayastats.com/en/two-engines-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>productivity</category>
      <category>git</category>
    </item>
    <item>
      <title>How I make Claude Code and Codex review each other's work</title>
      <dc:creator>Iliya Oblakov</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:19:43 +0000</pubDate>
      <link>https://dev.to/iliyaoblakov/how-i-make-claude-code-and-codex-review-each-others-work-5dmh</link>
      <guid>https://dev.to/iliyaoblakov/how-i-make-claude-code-and-codex-review-each-others-work-5dmh</guid>
      <description>&lt;p&gt;I build software for paying clients with Claude Code and Codex. Both are good at writing code. Neither is good at judging its own work, because the session that wrote a change is the worst place to decide whether that change is finished. It already believes it is.&lt;/p&gt;

&lt;p&gt;So I split the jobs. Claude Code writes. Codex reviews, read-only. My own tests run on the same commit. A small script decides whether anything merges, and it only looks at evidence saved to disk.&lt;/p&gt;

&lt;p&gt;Here is how to set it up. You need git, the Claude Code CLI, the Codex CLI and a test command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: one rulebook, two readers
&lt;/h2&gt;

&lt;p&gt;If the writer and the reviewer follow different rules, the reviewer flags things the writer was never told, and you end up arbitrating between two models.&lt;/p&gt;

&lt;p&gt;Keep one file, say &lt;code&gt;rules/RULES.md&lt;/code&gt;, with your real standards. Then point both engines at it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, which can import other files with an &lt;code&gt;@&lt;/code&gt; line. Put &lt;code&gt;@rules/RULES.md&lt;/code&gt; in it and you are done.&lt;/li&gt;
&lt;li&gt;Codex reads &lt;code&gt;AGENTS.md&lt;/code&gt;. Generate it from the same source rather than editing it by hand:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# sync-rules.sh: build AGENTS.md from the shared rulebook&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;!-- generated from rules/RULES.md, do not edit --&amp;gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;cat &lt;/span&gt;rules/RULES.md
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; rules/codex-extra.md &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;rules/codex-extra.md
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep engine specific lines few. Everything that defines "good work" belongs in the shared file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: write rules a reviewer can check
&lt;/h2&gt;

&lt;p&gt;This is where most setups go wrong. "Write clean, maintainable code" sounds sensible, but a reviewer cannot check a diff against it, so you get opinions instead of findings.&lt;/p&gt;

&lt;p&gt;A good rule names something visible in a diff, says what to do instead, and says how to tell it was done. Here is one example rule you can copy into your own rulebook. I wrote it for this post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Public signatures&lt;/span&gt;

When a change renames, removes or reorders the parameters of an
exported function, class or API route:
&lt;span class="p"&gt;
1.&lt;/span&gt; Update every caller in the repository in the same commit.
&lt;span class="p"&gt;2.&lt;/span&gt; Add a line to the commit message body starting with
   "Signature change:" that names the old and new signature.
&lt;span class="p"&gt;3.&lt;/span&gt; If a caller lives outside this repository, do not make the change.
   Add a new function next to the old one and mark the old one
   deprecated in a comment.

Reviewer check: search the diff for changed exported signatures.
For each one, confirm the callers were updated and the
"Signature change:" line exists. If either is missing, the verdict
is CHANGES REQUIRED and the finding quotes this rule.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the last paragraph. Each rule tells the reviewer what to look for. That turns review from taste into a checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: let Claude Code work in its own worktree
&lt;/h2&gt;

&lt;p&gt;Never let the writing agent touch your main checkout. A git worktree gives it a separate directory on a separate branch, sharing the same repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../task-login-fix &lt;span class="nt"&gt;-b&lt;/span&gt; task-login-fix
&lt;span class="nb"&gt;cd&lt;/span&gt; ../task-login-fix
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give Claude Code the task there. If it goes wrong, &lt;code&gt;git worktree remove&lt;/code&gt; throws the whole attempt away and your main branch never saw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Codex reviews read-only, with a forced verdict
&lt;/h2&gt;

&lt;p&gt;The reviewer must not be able to edit files. A reviewer that can fix what it finds is a second author, and nobody reviewed that second author. Run Codex with a read-only sandbox.&lt;/p&gt;

&lt;p&gt;Give it three things: the rulebook (it already has it through AGENTS.md), the diff against the base branch, and a prompt that ends with a strict output format. A minimal version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# review.sh: ask Codex for a verdict on the current commit&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;sha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .ai/reviews
git diff &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;...HEAD"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;".ai/reviews/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.diff"&lt;/span&gt;

codex &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--sandbox&lt;/span&gt; read-only &lt;span class="s2"&gt;"Review the diff in .ai/reviews/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.diff
against the rules in AGENTS.md. List each problem with the rule it
breaks, quoted. Do not edit any file. The last line of your answer
must be exactly ACCEPT or exactly CHANGES REQUIRED."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;".ai/reviews/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;

&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="s2"&gt;".ai/reviews/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here.&lt;/p&gt;

&lt;p&gt;First, the verdict file is named after the commit hash. If Claude Code adds one more commit after the review, the hash changes and the old ACCEPT no longer applies. You cannot accidentally merge code nobody reviewed.&lt;/p&gt;

&lt;p&gt;Second, the verdict is a fixed word on the last line. Scripts can read that. They cannot read "mostly looks good, a few small things".&lt;/p&gt;

&lt;p&gt;If the verdict is CHANGES REQUIRED, hand the review file back to Claude Code in the worktree, let it fix the findings, commit, and review again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: run your tests on the same commit
&lt;/h2&gt;

&lt;p&gt;The review checks the rules. Your tests check the behaviour. Run them on the exact commit that was reviewed and record a pass only when they pass cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;sha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .ai/tests
&lt;span class="k"&gt;if &lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pass &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;".ai/tests/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.pass"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;npm test&lt;/code&gt; for whatever your project uses. Add &lt;code&gt;.ai/&lt;/code&gt; to your git ignore so none of this ends up in the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: a dumb gate decides the merge
&lt;/h2&gt;

&lt;p&gt;The final step should involve no judgement at all. A script checks that both pieces of evidence exist for the current commit, and refuses otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# gate.sh: refuse to merge without a review and a test pass for HEAD&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;sha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;review&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".ai/reviews/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
&lt;span class="nv"&gt;tests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".ai/tests/&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;.pass"&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tests&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No test pass for &lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$review&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No review for &lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$review&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ACCEPT"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Review did not accept &lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Working tree not clean"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Gate passed for &lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it before every merge. "Reviewed and tested" stops being something you remember and becomes something a script checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;It does not make the code correct. Codex can miss things, and so can your tests. The gate only proves the steps happened on the commit you are about to merge. That is still a big improvement over trusting the same session that wrote the code to tell you it is done.&lt;/p&gt;

&lt;p&gt;A few habits that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send routine work to cheaper models and keep the expensive reasoning for design, hard debugging and the review itself.&lt;/li&gt;
&lt;li&gt;When a review keeps flagging the same thing, turn it into a rule with a reviewer check.&lt;/li&gt;
&lt;li&gt;Read the review, even when it says ACCEPT. The gate is a floor, not a replacement for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you want the full version
&lt;/h2&gt;

&lt;p&gt;Everything above is enough to start. If you would rather install a finished setup, I packaged the workflow I use on client work as the Two Engines Kit: a shared rulebook for both engines, cross review, gate and merge scripts (the merge script can be set to refuse without a saved ACCEPT), hooks and settings for both engines, a skill for long tasks, an optional shared memory module, and GUIDE.md on why each piece exists. The installer previews each change and asks before applying it, and uninstall.sh restores your old files byte for byte. It needs both Claude Code and Codex, and runs on WSL2, Linux or macOS (tested on WSL2 with Ubuntu 24.04).&lt;/p&gt;

&lt;p&gt;It is $29 for the first 30 sales, then $39: &lt;a href="https://rayastats.com/en/two-engines-kit" rel="noopener noreferrer"&gt;https://rayastats.com/en/two-engines-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>git</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
