<?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: Rapls</title>
    <description>The latest articles on DEV Community by Rapls (@rapls).</description>
    <link>https://dev.to/rapls</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%2F3886801%2F1f380f23-3b41-4825-80fe-ba6efc0c6d3e.png</url>
      <title>DEV Community: Rapls</title>
      <link>https://dev.to/rapls</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rapls"/>
    <language>en</language>
    <item>
      <title>My Claude Code auto mode config still said GITHUB_ACCOUNT_HERE. One command found it.</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Thu, 12 Nov 2026 15:34:04 +0000</pubDate>
      <link>https://dev.to/rapls/my-claude-code-auto-mode-config-still-said-githubaccounthere-one-command-found-it-2mb7</link>
      <guid>https://dev.to/rapls/my-claude-code-auto-mode-config-still-said-githubaccounthere-one-command-found-it-2mb7</guid>
      <description>&lt;p&gt;I've been running Claude Code in auto mode for a while. In auto mode, Claude doesn't stop to ask before every routine action. A separate classifier model looks at each action and blocks the risky ones.&lt;/p&gt;

&lt;p&gt;You can tell that classifier about your setup in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, under &lt;code&gt;autoMode&lt;/code&gt;. I had done that. I wrote down who I am, what's normal for my work, and what must never leave my machine.&lt;/p&gt;

&lt;p&gt;This week I printed that block to check it. One of my lines still said this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source control: GitHub personal account GITHUB_ACCOUNT_HERE; repositories under it are trusted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It had been a placeholder since the day I wrote the config. The classifier never knew which GitHub account was mine.&lt;/p&gt;

&lt;p&gt;Below is how I found it, and the two checks I'd now run on any auto mode config.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the &lt;code&gt;autoMode&lt;/code&gt; block is
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;autoMode&lt;/code&gt; block has four lists, and each one is plain English, not patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;environment&lt;/code&gt;: context about your setup. Who you are, which repos and domains you trust.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allow&lt;/code&gt;: exceptions. Things the classifier would normally block, but that are fine for you.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;soft_deny&lt;/code&gt;: things to block unless you clearly asked for them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hard_deny&lt;/code&gt;: things to block no matter what.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, the classifier trusts only the repo you're working in and its configured remotes. Anything else, like your other repos on the same account, is "outside" until you say otherwise in &lt;code&gt;environment&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's exactly what my placeholder line was supposed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: does every list you set still have &lt;code&gt;"$defaults"&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;This is the one that can actually hurt you.&lt;/p&gt;

&lt;p&gt;Each list ships with built-in rules. If you set a list and include the literal string &lt;code&gt;"$defaults"&lt;/code&gt;, your entries are added to the built-in ones. If you set a list &lt;em&gt;without&lt;/em&gt; it, your entries replace the built-in ones.&lt;/p&gt;

&lt;p&gt;The docs are blunt about what that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;soft_deny&lt;/code&gt; without &lt;code&gt;"$defaults"&lt;/code&gt; drops every built-in soft block, including force pushes, &lt;code&gt;curl | bash&lt;/code&gt;, production deploys, and auto mode bypass.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hard_deny&lt;/code&gt; without &lt;code&gt;"$defaults"&lt;/code&gt; drops the built-in data exfiltration rule.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allow&lt;/code&gt; without it drops the built-in exceptions. That's not dangerous, but you'll get more prompts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;environment&lt;/code&gt; is the exception. If you generate it with &lt;code&gt;/auto-mode-setup&lt;/code&gt;, it's written without &lt;code&gt;"$defaults"&lt;/code&gt; on purpose, because the draft spells out the built-in entries itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see what you wrote, print your &lt;code&gt;autoMode&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'.autoMode'&lt;/span&gt; ~/.claude/settings.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;soft_deny&lt;/code&gt;, &lt;code&gt;hard_deny&lt;/code&gt;, and &lt;code&gt;environment&lt;/code&gt; all started with &lt;code&gt;"$defaults"&lt;/code&gt;. That's also where I saw the placeholder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: what is the classifier actually using?
&lt;/h2&gt;

&lt;p&gt;Your settings file is what you wrote. It isn't necessarily what the classifier reads, because entries from different scopes are combined.&lt;/p&gt;

&lt;p&gt;This prints the effective config, your settings merged with the defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude auto-mode config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To confirm the built-in force-push rule is still there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude auto-mode config &lt;span class="o"&gt;|&lt;/span&gt; grep -c &lt;span class="s1"&gt;'Git Destructive'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything above 0 means it's in effect.&lt;/p&gt;

&lt;p&gt;To check the line I cared about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude auto-mode config &lt;span class="o"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;'Source control'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After I replaced &lt;code&gt;GITHUB_ACCOUNT_HERE&lt;/code&gt; with my real account name, this showed two lines. One is the built-in default, saying it trusts the current repo and its remotes. The other is mine, naming my account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more commands worth knowing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude auto-mode critique&lt;/code&gt; sends your custom entries to a model and flags ones that are ambiguous, redundant, or likely to cause false positives.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude auto-mode defaults&lt;/code&gt; prints the built-in rules. Read these before you ever write a list without &lt;code&gt;"$defaults"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you don't want to open a terminal, &lt;code&gt;/permissions&lt;/code&gt; inside a session has an &lt;strong&gt;Auto mode&lt;/strong&gt; tab where you can view and edit the same entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I missed it
&lt;/h2&gt;

&lt;p&gt;I wrote the config once, it looked complete, and nothing broke loudly. Auto mode is a judgment call by a model, so a missing piece of context doesn't produce an error. At most, it makes the classifier a little more cautious than it needs to be.&lt;/p&gt;

&lt;p&gt;That's the same pattern as every other silent failure I've hit with AI tools: the output looks finished, and nothing tells you otherwise until you check.&lt;/p&gt;

&lt;p&gt;One rule from the Claude Code best practices page fits here. Auto mode is a classifier, not a hard rule, and it can be wrong. So the things that must never happen still belong in &lt;code&gt;permissions.deny&lt;/code&gt; or hooks. The &lt;code&gt;autoMode&lt;/code&gt; block is for context, not for guarantees.&lt;/p&gt;

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

&lt;p&gt;If you use auto mode, run these once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'.autoMode'&lt;/span&gt; ~/.claude/settings.json          &lt;span class="c"&gt;# what you wrote&lt;/span&gt;
claude auto-mode config &lt;span class="o"&gt;|&lt;/span&gt; grep -c &lt;span class="s1"&gt;'Git Destructive'&lt;/span&gt;  &lt;span class="c"&gt;# built-in blocks still on?&lt;/span&gt;
claude auto-mode config                        &lt;span class="c"&gt;# what the classifier really reads&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read your &lt;code&gt;environment&lt;/code&gt; lines one more time, looking for anything that still looks like a template.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: The config lines in this post are from my own setup. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>I thought Claude Code would make my work lighter. I became its checker instead. Here's every file I wrote to stop</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:20:36 +0000</pubDate>
      <link>https://dev.to/rapls/i-thought-claude-code-would-make-my-work-lighter-i-became-its-checker-instead-heres-every-file-i-1ifl</link>
      <guid>https://dev.to/rapls/i-thought-claude-code-would-make-my-work-lighter-i-became-its-checker-instead-heres-every-file-i-1ifl</guid>
      <description>&lt;p&gt;I thought handing work to Claude Code would make my days lighter.&lt;/p&gt;

&lt;p&gt;I build and sell WordPress plugins on my own. The AI would write the code, I'd ask for things and collect the results. Two extra hands.&lt;/p&gt;

&lt;p&gt;What actually started was a different job.&lt;/p&gt;

&lt;p&gt;Claude Code would say "Done," and I'd open the release zip to find files that should never ship. It would say "Tests pass," and I'd ask which tests. Every permission prompt, I'd read the command and press "Yes." I had gone from the person who writes the code to the person who checks the AI's work, one item at a time.&lt;/p&gt;

&lt;p&gt;This post is every file I added to get out of that role, from the development of my caching plugin, Prime Cache: the CLAUDE.md, the permission settings, the hook I added after an incident, and how I hand work over between sessions. For each piece, I'll say why the line is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted to hand off was checking, not writing
&lt;/h2&gt;

&lt;p&gt;Getting an AI to write code isn't the hard part anymore. Checking that the code is right is. If I do all the checking, my hands are never free.&lt;/p&gt;

&lt;p&gt;So I changed how I write instructions. Along with "write this," I add "when you're done, run this. If the output is empty, it passed." If the AI checks its own work and shows me the evidence, I only have to read the evidence.&lt;/p&gt;

&lt;p&gt;The Claude Code team described the same idea for their own process in "How the Claude Code team does code review" (October 15, 2026): Claude reviews and fixes a change before a person is asked to look, and human attention goes to the riskier changes. I work alone, so instead of a team process, I put it in files.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CLAUDE.md: write the reason next to the rule
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md is the file Claude Code reads first when it opens a project. Mine for Prime Cache is about 250 lines. It didn't start that long; a line got added each time something went wrong.&lt;/p&gt;

&lt;p&gt;Prime Cache saves pages as HTML files and serves them before WordPress loads. That means one file runs before WordPress exists:&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;### Important Constraints&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`dropins/page-cache.php`&lt;/span&gt; runs before WordPress — cannot use any WP functions (no &lt;span class="sb"&gt;`get_option()`&lt;/span&gt;, &lt;span class="sb"&gt;`wp_parse_url()`&lt;/span&gt;, etc.)
&lt;span class="p"&gt;-&lt;/span&gt; Atomic writes (temp file + rename) prevent serving partial files under concurrency
&lt;span class="p"&gt;-&lt;/span&gt; Path traversal protection via &lt;span class="sb"&gt;`realpath()`&lt;/span&gt; validation against cache directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before I wrote this, Claude would sometimes helpfully reach for &lt;code&gt;get_option()&lt;/code&gt; in that file. It's correct in an ordinary plugin file. Here it doesn't work. The AI wasn't wrong, it just didn't know.&lt;/p&gt;

&lt;p&gt;The reason matters more than the rule. "Don't use WP functions" alone, and the AI looks for a similar function. "Because it runs before WordPress," and it can work out on its own that the similar function is out too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Say what's banned, and what isn't
&lt;/h3&gt;

&lt;p&gt;I keep two other caching plugins in my dev WordPress to study how they work. The AI is very good at "referencing" code that's lying around: copy a function body, rename the variables. So my CLAUDE.md has an originality policy, and the important half is the second list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Prohibited:
&lt;span class="p"&gt;-&lt;/span&gt; Copying a function/method body and only renaming variables or constants
&lt;span class="p"&gt;-&lt;/span&gt; Copying comment blocks or docblocks
&lt;span class="p"&gt;-&lt;/span&gt; Directly mimicking multi-line code structure

Acceptable similarities:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`wp_cache_*`&lt;/span&gt; signatures required by the WordPress drop-in API
&lt;span class="p"&gt;-&lt;/span&gt; WordPress hook names (&lt;span class="sb"&gt;`transition_post_status`&lt;/span&gt;, &lt;span class="sb"&gt;`create_term`&lt;/span&gt;, ...)
&lt;span class="p"&gt;-&lt;/span&gt; Checks for standard WordPress constants (&lt;span class="sb"&gt;`DONOTCACHEPAGE`&lt;/span&gt;, &lt;span class="sb"&gt;`WP_CACHE`&lt;/span&gt;, ...)
&lt;span class="p"&gt;-&lt;/span&gt; Industry-standard patterns (atomic write, gzip, 304 responses)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The real file is in Japanese, and it names the plugins I'm studying.)&lt;/p&gt;

&lt;p&gt;Write only "don't copy," and the AI starts avoiding WordPress's required function names too, which breaks things. With examples on both sides, it can draw the line between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The release zip, and "empty means pass"
&lt;/h3&gt;

&lt;p&gt;This is the part I most want to share.&lt;/p&gt;

&lt;p&gt;My CLAUDE.md has the zip command with its exclude list, and then a check that must be run afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-Z1&lt;/span&gt; prime-cache.zip | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;gitattributes|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;claude|/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;git/|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;serena|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;DS_Store|CLAUDE.md|/tests/|/docs/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lists the files in the zip and looks for names that must not be there. CLAUDE.md says the result "must be empty."&lt;/p&gt;

&lt;p&gt;That's the smallest form of handing off the checking. If Claude only says "I built the zip," I have to open it. If it says "I built the zip, and the check came back empty," I read one line. The pass condition is "empty," which neither the AI nor I can misread.&lt;/p&gt;

&lt;p&gt;Two notes in that section exist because of real near-misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested files.&lt;/strong&gt; claude-mem, a plugin I use for session memory, drops &lt;code&gt;.claude/&lt;/code&gt; folders and &lt;code&gt;CLAUDE.md&lt;/code&gt; files into subfolders like &lt;code&gt;languages/.claude/&lt;/code&gt; and &lt;code&gt;includes/CLAUDE.md&lt;/code&gt;. My first exclude list only covered the top level, so these leaked. The excludes now use &lt;code&gt;*/.claude/*&lt;/code&gt; and &lt;code&gt;**/CLAUDE.md&lt;/code&gt; forms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git-ignored isn't zip-ignored.&lt;/strong&gt; &lt;code&gt;tests/&lt;/code&gt; is in &lt;code&gt;.gitignore&lt;/code&gt;, but the zip is built from the working folder. The &lt;code&gt;-x&lt;/code&gt; exclude is what actually keeps it out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the Pro add-on, there's one block marked "MUST exclude": tooling that only I use on my side for licenses. CLAUDE.md says what to exclude and why it matters, and the zip check fails if any of it shows up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the places that broke before
&lt;/h3&gt;

&lt;p&gt;Near the end of CLAUDE.md is a pre-release checklist. One item says: with the Pro add-on installed and Cloudflare/Sucuri API keys saved, open the generated config file and confirm the keys are NOT written there. The free plugin skips them on purpose, and the line ends with "this is the regression case to protect."&lt;/p&gt;

&lt;p&gt;Leaving the keys out was deliberate. Someday someone (an AI) tidying up the code might decide that line isn't needed. This item is how I'd notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. AGENTS.md: a find-and-replace copy that broke silently
&lt;/h2&gt;

&lt;p&gt;While writing this up, I compared files and found a mistake I should be honest about.&lt;/p&gt;

&lt;p&gt;I also use OpenAI's Codex, which reads AGENTS.md, not CLAUDE.md. I had made AGENTS.md by copying CLAUDE.md. Comparing them, every "claude" had been replaced with "Codex":&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="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"prime-cache/.Codex/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"prime-cache/*/.Codex/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"prime-cache/AGENTS.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"prime-cache/**/AGENTS.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks adapted for Codex. But now &lt;code&gt;.claude/&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; aren't excluded. If Codex built a release from these instructions, Claude Code's settings and instructions would ship in the zip.&lt;/p&gt;

&lt;p&gt;Worse, the check was replaced the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-Z1&lt;/span&gt; prime-cache.zip | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;gitattributes|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;Codex|/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;git/|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;serena|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;DS_Store|AGENTS.md|..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't look for &lt;code&gt;.claude&lt;/code&gt; or &lt;code&gt;CLAUDE.md&lt;/code&gt;, so even if they were in the zip, the result would be empty. A pass.&lt;/p&gt;

&lt;p&gt;The exclude and the check broke together, from the same replacement, so neither one complains. The copy also had "Codex-mem" (a plugin that doesn't exist) and "Codex.ai/code".&lt;/p&gt;

&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When you copy an instruction file for another agent, reread and rewrite it; don't find-and-replace. The exclude list should cover both agents' files, so either one can build the zip safely.&lt;/li&gt;
&lt;li&gt;The check needs checking too. "Empty means pass" is only as good as the names it searches for. Feed it a zip that deliberately contains &lt;code&gt;.claude/&lt;/code&gt;, once, and see whether it fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. settings.json: stop it with mechanisms, not requests
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md is a request. Claude follows it almost every time. Almost.&lt;/p&gt;

&lt;p&gt;What must never happen goes into &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, in three boxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;allow&lt;/strong&gt; is what Claude can do without asking: read, search, edit, &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git diff&lt;/code&gt;, run tests and linters, compile translations. If these aren't here, Claude asks every time, and pressing "Yes" all day was exactly the checker job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ask&lt;/strong&gt; has only four entries in my setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(svn commit:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(svn ci:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(gh release create:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push --tags:*)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of them lead to a release. &lt;code&gt;svn commit&lt;/code&gt; publishes to WordPress.org: every site using the plugin gets an update notice, and it can't be taken back. The fewer things that ask, the more seriously I read the ones that do. Four kinds of prompt means that when one shows up, my hand stops: this is a release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deny&lt;/strong&gt; is what must not happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Read(**/.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Read(~/.ssh/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Edit(~/.claude/settings.json)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Edit(~/.claude/hooks/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(wget:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(sudo:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git reset --hard:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push --force:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(wp plugin delete:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(wp db drop:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"Bash(wp eval:*)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(There are more.) The settings file and the hooks can't be edited, so the AI can't decide "this restriction is in the way, so I removed it." A lock you can lift off with the key inside isn't a lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sandbox: decide which roads are open
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"failIfUnavailable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowedDomains"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"registry.npmjs.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"packagist.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wordpress.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.wordpress.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strictAllowlist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The real list has a few more npm and Composer hosts.) &lt;code&gt;failIfUnavailable&lt;/code&gt; means: if the sandbox can't start, refuse to run instead of running without it. The scariest case is the fence being broken and things running with no fence at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto mode: tell it what's normal here
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;defaultMode&lt;/code&gt; is &lt;code&gt;auto&lt;/code&gt;, where Claude judges for itself whether an action needs asking. I describe my environment for it, in plain sentences. Paraphrased:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapls Works is a one-person WordPress plugin business.&lt;/li&gt;
&lt;li&gt;Production sites, their databases, and the WordPress.org SVN are to be handled carefully. An SVN commit ships to every user and can't be undone. Release only when I explicitly say so.&lt;/li&gt;
&lt;li&gt;Stripe keys, Pro licenses, customer data, &lt;code&gt;.env&lt;/code&gt;, and &lt;code&gt;wp-config.php&lt;/code&gt; must never leave this machine or go into Git.&lt;/li&gt;
&lt;li&gt;There's no staging environment. Some folders in the working tree are deliberately untracked; if they're deleted, Git can't bring them back.&lt;/li&gt;
&lt;li&gt;Commits, branches, local merges and rebases, builds, tests, and pushing to my own GitHub are routine. No need to ask.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last line is the one that ends the checker job. Write only what's dangerous, and the AI checks everything with you. Write what's routine too, and it asks only what it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A hook: never lose the folder twice
&lt;/h2&gt;

&lt;p&gt;Hooks run my own script right before Claude Code executes a command. Mine starts like this:&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;# Blocks shell commands that could delete, move, or overwrite the Prime Cache&lt;/span&gt;
&lt;span class="c"&gt;# plugin *development* folders (prime-cache / prime-cache-pro) or otherwise wipe&lt;/span&gt;
&lt;span class="c"&gt;# the working tree. Added after the 2026-07-19 folder-loss incident.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes. I lost it once.&lt;/p&gt;

&lt;p&gt;deny already had dangerous commands, but there are many ways to write them. Block &lt;code&gt;rm -rf&lt;/code&gt; and &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;rsync --delete&lt;/code&gt;, and &lt;code&gt;find -delete&lt;/code&gt; are still open. So the hook blocks a command when it contains both a destructive operation and a dev folder name:&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;destructive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'rm[[:space:]]+-[a-z]*r|(^|[[:space:]])rmdir[[:space:]]|(^|[[:space:]])mv[[:space:]]|(^|[[:space:]])trash[[:space:]]|-delete([[:space:]]|$)|-exec[[:space:]]+rm|rsync[[:space:]].*--delete|git[[:space:]]+clean'&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$low&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destructive&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$low&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;deny &lt;span class="s2"&gt;"Blocked: this command could delete, move, or overwrite the Prime Cache plugin development folder ..."&lt;/span&gt;
  &lt;span class="k"&gt;fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't block &lt;code&gt;prime-cache.zip&lt;/code&gt;.&lt;/strong&gt; Building a release deletes the old zip first. A folder name followed by &lt;code&gt;.&lt;/code&gt; is treated as something else, so the daily release work isn't blocked. A guard that blocks normal work ends up getting removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the script breaks, it lets things through.&lt;/strong&gt; A bug in the guard must never wedge the session (fail-open).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the opposite of the sandbox, which fails closed. The sandbox is the last fence, so if it's broken, stop. The hook is one guard inside that fence, so if it's broken, step aside. Where to stop and where to let through is decided by role.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handovers: make the new session start itself
&lt;/h2&gt;

&lt;p&gt;Long work eventually needs a fresh session, and the new session doesn't know what we decided. Explaining it all again is checker work too. I write a HANDOFF.md and open the new session with one request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read HANDOFF.md. Then run /init to create CLAUDE.md, and make sure it includes chapter 3 (decisions) and 3.3 (what we decided not to do). When you're done, start with the request in chapter 8.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/init&lt;/code&gt; writes what it read into a file, so it survives the next session too. "What we decided not to do" is named explicitly because it's the first thing lost in a handover. The reasons were discussed mid-conversation, so the document tends to be short there, and a new session will kindly do exactly what we ruled out.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Ask for evidence with "done"
&lt;/h2&gt;

&lt;p&gt;The last piece is how "done" arrives. Here is what I'd add to the end of a CLAUDE.md:&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;## When you finish, report in this form&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; What you did: each changed file and what changed, one line each
&lt;span class="p"&gt;-&lt;/span&gt; What you checked: the commands you ran and their results ("empty", "12 of 12 passed")
&lt;span class="p"&gt;-&lt;/span&gt; What you did NOT check: anything time or environment kept you from verifying
&lt;span class="p"&gt;-&lt;/span&gt; What caught your eye: things you didn't fix but that seem worth a look
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third item matters most. An AI won't volunteer what it didn't check, so you assume it checked everything. Give it a field for that, and it fills it in. Then I check only what's listed there.&lt;/p&gt;

&lt;p&gt;One more thing: the same session reviewing its own work tends to go easy on it. For big changes I also have another Claude session, or Codex, look. From outside the session that wrote it, you see things the writer can't, like the find-and-replace mistake above. I only found that one because I put the two files side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CLAUDE.md&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project-specific constraints ("runs before WordPress"), with reasons&lt;/li&gt;
&lt;li&gt;What's banned, with examples of both banned and acceptable&lt;/li&gt;
&lt;li&gt;A check command to run when finished, and its pass condition&lt;/li&gt;
&lt;li&gt;Places that broke before, kept as pre-release checks&lt;/li&gt;
&lt;li&gt;The shape of the completion report, including "what you did not check"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Other agents' instruction files (AGENTS.md etc.)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not made by find-and-replace; names and exclude lists make sense&lt;/li&gt;
&lt;li&gt;The check has been fed a deliberately bad zip, once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;settings.json&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only and verification actions in allow&lt;/li&gt;
&lt;li&gt;Only irreversible actions in ask&lt;/li&gt;
&lt;li&gt;Secrets, the settings file itself, and history rewrites in deny&lt;/li&gt;
&lt;li&gt;A sandbox with an allowlist of hosts&lt;/li&gt;
&lt;li&gt;In auto mode, routine work described alongside the dangers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hooks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A guard where something actually went wrong once&lt;/li&gt;
&lt;li&gt;Normal work isn't blocked&lt;/li&gt;
&lt;li&gt;A broken guard doesn't stop all work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Handovers&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The handover document has a "what we decided not to do" chapter&lt;/li&gt;
&lt;li&gt;The first message of a new session covers read, write it down, and start the next task&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost every line in these files was added after something went wrong. You don't need a perfect CLAUDE.md on day one. When the AI gets something wrong, add one line instead of getting annoyed, and put in it why it's wrong and how to check.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: The files in this post are from my own project; I quoted and translated the relevant parts for this post. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>My Cursor state.vscdb was 8.9 GB. You can work out how much VACUUM will save before you run it</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Sun, 01 Nov 2026 04:35:58 +0000</pubDate>
      <link>https://dev.to/rapls/my-cursor-statevscdb-was-89-gb-you-can-work-out-how-much-vacuum-will-save-before-you-run-it-3cn</link>
      <guid>https://dev.to/rapls/my-cursor-statevscdb-was-89-gb-you-can-work-out-how-much-vacuum-will-save-before-you-run-it-3cn</guid>
      <description>&lt;p&gt;My Mac was running low on disk space, so I went through the big folders one at a time. One of them was Cursor's &lt;code&gt;globalStorage&lt;/code&gt;, at 19 GB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Application Support/Cursor/User/globalStorage
  8.9G  state.vscdb
  8.9G  state.vscdb.backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;state.vscdb&lt;/code&gt; is a SQLite database. It holds Cursor's settings and state, including the agent chat history. The usual advice is to quit Cursor and run &lt;code&gt;sqlite3 state.vscdb "VACUUM;"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before you do that, you can work out how much it will shrink. Multiply &lt;code&gt;page_size&lt;/code&gt; by &lt;code&gt;freelist_count&lt;/code&gt; and you have the space VACUUM will give back. I built a 574 MiB test database, deleted two thirds of the rows, and predicted 191.4 MiB. After VACUUM it was 191.5 MiB.&lt;/p&gt;

&lt;p&gt;I'm not the first to write about this file. The Cursor forum has several threads about &lt;code&gt;state.vscdb&lt;/code&gt; reaching tens of gigabytes, and the quit-then-VACUUM fix is posted there. The SQLite docs also say VACUUM needs temporary disk space. What I'm adding is a set of measurements on a WAL-mode database shaped like Cursor's: the prediction, how much free disk VACUUM used while running, and when it refuses to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting rows doesn't shrink the file
&lt;/h2&gt;

&lt;p&gt;When SQLite deletes data, the file stays the same size. The freed pages go on a "freelist" and get reused by later writes. VACUUM rebuilds the file without them.&lt;/p&gt;

&lt;p&gt;You can see the count without changing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Library/Application&lt;span class="se"&gt;\ &lt;/span&gt;Support/Cursor/User/globalStorage
sqlite3 state.vscdb &lt;span class="s2"&gt;"PRAGMA page_size; PRAGMA page_count; PRAGMA freelist_count;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only reads the file, so you can run it while Cursor is open.&lt;/p&gt;

&lt;p&gt;My test database had the same two tables as Cursor's (&lt;code&gt;ItemTable&lt;/code&gt; and &lt;code&gt;cursorDiskKV&lt;/code&gt;) and used WAL mode, like the real file (mine had &lt;code&gt;-wal&lt;/code&gt; and &lt;code&gt;-shm&lt;/code&gt; files next to it). I inserted 3,000 rows of 200 KB each, then deleted 2,000 of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;after insert:        574.3 MiB
after deleting 2/3:  574.3 MiB   &amp;lt;- no change

page_size       4096
page_count      147030
freelist_count  98017
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4096 × 98017 is about 383 MiB. Take that from 574.3 MiB and you get 191.4 MiB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;after VACUUM:        191.5 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also ran VACUUM on a copy where I hadn't deleted anything. &lt;code&gt;freelist_count&lt;/code&gt; was 0 and the file stayed at 574.3 MiB. If your freelist is small, VACUUM won't help much; the file really is that full of data.&lt;/p&gt;

&lt;h2&gt;
  
  
  VACUUM needs about twice the final size in free space
&lt;/h2&gt;

&lt;p&gt;VACUUM builds a new copy of the database and then writes it back. While that happens, it uses disk.&lt;/p&gt;

&lt;p&gt;I watched &lt;code&gt;df&lt;/code&gt; while the 574.3 MiB database was being vacuumed. At the peak, disk usage was 384 MiB higher than before I started. That's about twice the final size (191.5 MiB).&lt;/p&gt;

&lt;p&gt;The WAL file accounts for half of it: &lt;code&gt;-wal&lt;/code&gt; grew to 192.6 MiB. The other half is most likely the temporary copy. SQLite deletes its temp file from the directory as soon as it opens it, so &lt;code&gt;du&lt;/code&gt; never saw it. Only &lt;code&gt;df&lt;/code&gt; did.&lt;/p&gt;

&lt;p&gt;For Cursor, this matters if the file won't shrink much. If 8.9 GB only drops to 8 GB, you'd need around 16 GB free while it runs. If your disk is nearly full, clear something else first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quit Cursor first
&lt;/h2&gt;

&lt;p&gt;I also tried running VACUUM while another connection had the file open:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Other connection&lt;/th&gt;
&lt;th&gt;VACUUM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;open, idle&lt;/td&gt;
&lt;td&gt;succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inside a read transaction&lt;/td&gt;
&lt;td&gt;succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inside a write transaction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Error: stepping, database is locked (5)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In WAL mode, a reader didn't block VACUUM. A writer did. Cursor writes to this database while you use it, so quit it fully (Cmd+Q) before you run VACUUM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The .backup file
&lt;/h2&gt;

&lt;p&gt;A reply on the Cursor forum describes &lt;code&gt;state.vscdb.backup&lt;/code&gt; as a backup Cursor makes automatically, and says it should shrink once the main file does. I didn't find anything about when it's created.&lt;/p&gt;

&lt;p&gt;In my case, VACUUM brought &lt;code&gt;state.vscdb&lt;/code&gt; down to [TO FILL IN: size after VACUUM]. I started Cursor, checked that it worked normally, and then deleted the &lt;code&gt;.backup&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Library/Application&lt;span class="se"&gt;\ &lt;/span&gt;Support/Cursor/User/globalStorage
sqlite3 state.vscdb &lt;span class="s2"&gt;"VACUUM;"&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; state.vscdb
&lt;span class="c"&gt;# start Cursor, check it works, then:&lt;/span&gt;
&lt;span class="nb"&gt;rm &lt;/span&gt;state.vscdb.backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If VACUUM doesn't shrink it, the same forum thread describes deleting chat-history keys from &lt;code&gt;cursorDiskKV&lt;/code&gt; and then running VACUUM again. That removes old chats from the UI, so I haven't tried it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Work out the gain first: &lt;code&gt;page_size × freelist_count&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A small &lt;code&gt;freelist_count&lt;/code&gt; means VACUUM won't free much&lt;/li&gt;
&lt;li&gt;Have about twice the final size free while it runs&lt;/li&gt;
&lt;li&gt;Quit Cursor; a writer makes VACUUM fail with &lt;code&gt;database is locked&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delete &lt;code&gt;state.vscdb.backup&lt;/code&gt; only after Cursor starts fine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How big is your &lt;code&gt;globalStorage&lt;/code&gt; right now?&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mk.py: a WAL-mode database shaped like Cursor's
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pragma journal_mode=wal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create table ItemTable(key text unique on conflict replace, value blob)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create table cursorDiskKV(key text unique on conflict replace, value blob)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;insert into cursorDiskKV values(?,?)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bubbleId:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pragma wal_checkpoint(truncate)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 mk.py s.db
sqlite3 s.db &lt;span class="s2"&gt;"delete from cursorDiskKV where cast(substr(key,10) as int) % 3 != 0;
              pragma wal_checkpoint(truncate);"&lt;/span&gt;
sqlite3 s.db &lt;span class="s2"&gt;"pragma page_size; pragma page_count; pragma freelist_count;"&lt;/span&gt;
sqlite3 s.db &lt;span class="s2"&gt;"VACUUM;"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; s.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran these with SQLite 3.45.1 on Linux. The real Cursor file on a Mac may not give the same numbers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: The tests were run on a test machine I set up for this post. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>sqlite</category>
      <category>macos</category>
      <category>productivity</category>
    </item>
    <item>
      <title>php -v over SSH doesn’t tell you which PHP your website runs</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Sat, 24 Oct 2026 03:55:47 +0000</pubDate>
      <link>https://dev.to/rapls/php-v-over-ssh-doesnt-tell-you-which-php-your-website-runs-26b3</link>
      <guid>https://dev.to/rapls/php-v-over-ssh-doesnt-tell-you-which-php-your-website-runs-26b3</guid>
      <description>&lt;p&gt;After a PHP release, the quickest check is to SSH in and type &lt;code&gt;php -v&lt;/code&gt;. I've done that myself. For this post I set up a test machine to see how much that number says about the website. Not much.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;php -v&lt;/code&gt; reports the command-line PHP. Your site is served by whatever PHP the web server hands requests to, and on the same box those can be different builds, different versions, with different extensions and different php.ini files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same machine, two answers
&lt;/h2&gt;

&lt;p&gt;I put this together on Ubuntu 24.04: Apache 2.4.58 passing &lt;code&gt;.php&lt;/code&gt; requests to PHP-FPM 8.3, and the &lt;code&gt;php&lt;/code&gt; command pointing at the PHP 8.4 CLI that was already installed. Then I asked each side who it was.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How I checked&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;
&lt;code&gt;php -v&lt;/code&gt; over SSH&lt;/td&gt;
&lt;td&gt;PHP 8.4.21 (cli)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PHP_VERSION&lt;/code&gt; from a page in the browser&lt;/td&gt;
&lt;td&gt;8.3.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PHP_SAPI&lt;/code&gt; on the command line&lt;/td&gt;
&lt;td&gt;cli&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PHP_SAPI&lt;/code&gt; in the browser&lt;/td&gt;
&lt;td&gt;fpm-fcgi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;php.ini loaded on the command line&lt;/td&gt;
&lt;td&gt;/etc/php/8.4/cli/php.ini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;php.ini loaded in the browser&lt;/td&gt;
&lt;td&gt;/etc/php/8.3/fpm/php.ini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress Site Health, opened in the browser&lt;/td&gt;
&lt;td&gt;8.3.6, fpm-fcgi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same Site Health data, read from the CLI&lt;/td&gt;
&lt;td&gt;8.4.21, cli&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this is new. The PHP manual's chapter on the command line covers the separate SAPI and the separate php.ini. Seeing the two side by side on one machine shows how easy it is to check the wrong one and call it done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why they drift apart
&lt;/h2&gt;

&lt;p&gt;On Ubuntu, each PHP version and each SAPI is its own package: &lt;code&gt;php8.3-cli&lt;/code&gt;, &lt;code&gt;php8.3-fpm&lt;/code&gt;, &lt;code&gt;php8.4-cli&lt;/code&gt;, and so on. The bare &lt;code&gt;php&lt;/code&gt; command is a symlink to one of them.&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;$ &lt;/span&gt;which &lt;span class="nt"&gt;-a&lt;/span&gt; php php8.3 php8.4
/usr/bin/php
/usr/bin/php8.3
/usr/bin/php8.4
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /usr/bin/php
/usr/bin/php -&amp;gt; /etc/alternatives/php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here it pointed at 8.4. The web server config sent requests to &lt;code&gt;php-fpm8.3&lt;/code&gt;. Those two choices are made in different places, by different people sometimes, and nothing keeps them in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  It wasn't only the version
&lt;/h2&gt;

&lt;p&gt;Halfway through, WordPress refused to load in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your PHP installation appears to be missing the MySQL extension which is required by WordPress.
Please check that the mysqli PHP extension is installed and enabled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the command line, the same WordPress install worked fine. The 8.4 CLI had &lt;code&gt;mysqli&lt;/code&gt;. The 8.3 FPM didn't. &lt;code&gt;php -m&lt;/code&gt; happily listed &lt;code&gt;mysqli&lt;/code&gt;, because &lt;code&gt;php -m&lt;/code&gt; was asking the CLI. Installing &lt;code&gt;php8.3-mysql&lt;/code&gt; and restarting FPM fixed the browser side.&lt;/p&gt;

&lt;p&gt;So the gap covers extensions and ini settings as well as the version number. A check run from the CLI only tells you about the CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the PHP your site actually runs
&lt;/h2&gt;

&lt;p&gt;The answer has to come from a web request.&lt;/p&gt;

&lt;p&gt;A tiny file does it. Give it a name nobody will guess, open it in the browser, then delete it:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: text/plain'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PHP_VERSION=%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;PHP_SAPI=%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;php.ini=%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;PHP_SAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;php_ini_loaded_file&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHP_VERSION=8.3.6
PHP_SAPI=fpm-fcgi
php.ini=/etc/php/8.3/fpm/php.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;phpinfo()&lt;/code&gt; tells you the same and much more. I prefer the small file, because forgetting to delete it costs less.&lt;/p&gt;

&lt;p&gt;On WordPress you don't need a file at all. Tools → Site Health → Info → Server shows the PHP version and SAPI, and since you open it in the browser, it's the web PHP.&lt;/p&gt;

&lt;p&gt;The catch: WP-CLI runs on the CLI PHP. When I pulled the same Site Health data from the command line, it reported 8.4.21 and &lt;code&gt;cli&lt;/code&gt;. If you check from WP-CLI, you're checking the wrong PHP again, just more politely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== Web
php_version: 8.3.6 (Supports 64bit values)
php_sapi: fpm-fcgi
== CLI
php_version: 8.4.21 (Supports 64bit values)
php_sapi: cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I check now
&lt;/h2&gt;

&lt;p&gt;When I want to know whether a PHP update reached my site, I check from the browser side: the small file, &lt;code&gt;phpinfo()&lt;/code&gt;, or Site Health. &lt;code&gt;php -v&lt;/code&gt; and WP-CLI tell me about the shell. That's still useful, for cron jobs and CLI scripts, but it isn't the answer to "is my website patched?"&lt;/p&gt;

&lt;p&gt;One thing I didn't test: what happens when the FPM package updates but the FPM service hasn't restarted yet. I'd expect the old binary to keep serving until a restart, but I haven't measured it, so I won't claim it.&lt;/p&gt;

&lt;p&gt;What does &lt;code&gt;php -v&lt;/code&gt; say on your server, and does it match what your website reports?&lt;/p&gt;




&lt;p&gt;I build WordPress plugins and write about them at &lt;a href="https://raplsworks.com/" rel="noopener noreferrer"&gt;https://raplsworks.com/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclaimer: The tests were run on a test machine I set up for this post. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>wordpress</category>
      <category>devops</category>
    </item>
    <item>
      <title>Claude Code now blocks the git command that almost cost me a day's work. I'm keeping my deny rules anyway.</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Thu, 25 Jun 2026 00:53:28 +0000</pubDate>
      <link>https://dev.to/rapls/claude-code-now-blocks-the-git-command-that-almost-cost-me-a-days-work-im-keeping-my-deny-rules-100g</link>
      <guid>https://dev.to/rapls/claude-code-now-blocks-the-git-command-that-almost-cost-me-a-days-work-im-keeping-my-deny-rules-100g</guid>
      <description>&lt;p&gt;A while back I asked an agent to roll back to my last push so I could glance at some old code. It reached for &lt;code&gt;git reset --hard&lt;/code&gt; and took my uncommitted work with it. After that near-miss I started adding deny rules to my &lt;code&gt;settings.json&lt;/code&gt; by hand: the boring lines that refuse the destructive git commands I never want run without me asking for them by name.&lt;/p&gt;

&lt;p&gt;This month, Claude Code shipped the guardrail I had been building by hand. According to the June 19, 2026 changelog, auto mode now blocks the destructive ones by default. &lt;code&gt;git reset --hard&lt;/code&gt;, &lt;code&gt;git checkout -- .&lt;/code&gt;, &lt;code&gt;git clean -fd&lt;/code&gt;, and &lt;code&gt;git stash drop&lt;/code&gt; are refused when you didn't ask to discard local work. &lt;code&gt;git commit --amend&lt;/code&gt; is blocked when the commit wasn't made by the agent this session. And &lt;code&gt;terraform&lt;/code&gt;, &lt;code&gt;pulumi&lt;/code&gt;, and &lt;code&gt;cdk&lt;/code&gt; destroy are blocked unless you named the specific stack.&lt;/p&gt;

&lt;p&gt;My first reaction was to delete my own lines. The default now does the thing I had been doing manually, so why keep the maintenance? Then I read what the guard actually is, and I put my deny rules back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard is real, and it's a classifier, not a rule
&lt;/h2&gt;

&lt;p&gt;Here is the part that changed my mind. In auto mode, Claude Code doesn't ask you to approve each command. A safety classifier, a second model reading the conversation, decides in real time whether each action is safe. The new guardrail lives inside that classifier. When it blocks &lt;code&gt;git reset --hard&lt;/code&gt;, it isn't matching a fixed pattern. It is inferring, from the transcript, that you didn't ask to throw work away.&lt;/p&gt;

&lt;p&gt;That inference is good, and it is welcome, and it is not the same thing as a rule. A classifier judges intent, which means it can also misjudge it. The honest tell is in the failure mode: when the classifier model is temporarily unavailable, auto mode fails closed and blocks commands until it comes back. Failing closed is the right call, and it is also a reminder that this protection is a live inference with a dependency, not a static line that is simply true whether or not a model is reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three reasons my hand-written lines stayed
&lt;/h2&gt;

&lt;p&gt;The first is mode. This guardrail is an auto-mode feature. If you run Claude Code with normal permission prompts, or in any setup where the auto-mode classifier isn't the thing gating commands, the default isn't doing this for you. A deny rule in &lt;code&gt;settings.json&lt;/code&gt;, or a &lt;code&gt;PreToolUse&lt;/code&gt; hook, runs regardless of mode. It doesn't care how you happen to be driving the agent today.&lt;/p&gt;

&lt;p&gt;The second is determinism. The classifier infers whether you asked; a deny rule matches whether you did. For most commands, inference is fine, and for the one command that would cost me a day, I want the boring certainty of a pattern that either fires or doesn't. &lt;code&gt;PreToolUse&lt;/code&gt; hooks also run before the permission system, so they catch things that slip past softer checks. Inference raises the floor. A matched rule is the floor.&lt;/p&gt;

&lt;p&gt;The third is scope. The new guard covers git and infrastructure-as-code. That is exactly the right place to start, and it is not the whole map. It does not stand between an agent and &lt;code&gt;rm -rf&lt;/code&gt;, a dropped database, a force-push to main, or a secret read out of a file and sent somewhere. Some of the near-misses I worry about most aren't git at all, and for those, the default did nothing, because it was never meant to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inference is not the only layer, and neither is the sandbox
&lt;/h2&gt;

&lt;p&gt;It is worth saying that the sandbox doesn't cover this either, for a subtle reason. Auto mode's sandboxing isolates the filesystem and network, but &lt;code&gt;git reset --hard&lt;/code&gt; looks safe to a sandbox: it only touches files inside your project directory. The boundary was never crossed. The work is just gone. Destructiveness and containment are different questions, and a tool can answer one perfectly while leaving the other open.&lt;/p&gt;

&lt;p&gt;So I stopped thinking of these as competing options and started stacking them. The built-in classifier is intent inference. My deny rules and hooks are deterministic enforcement. The sandbox is containment. Each one misses what the others catch, and the cheapest near-miss to prevent is the one three layers refuse instead of one.&lt;/p&gt;

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

&lt;p&gt;I didn't delete the lines the guard now duplicates. I left them as the deterministic backstop under a probabilistic improvement, which is what defense-in-depth is. What I did change is where I spent the attention the new default freed up: on the gaps it doesn't cover. The git case is handled three ways now. The &lt;code&gt;rm -rf&lt;/code&gt;, the database, and the secret-exfiltration cases are the ones still leaning on rules I wrote, so that's where the next hour of config goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note to my next self
&lt;/h2&gt;

&lt;p&gt;A better default is a reason to audit your config, not to delete it. When the floor rises, check what is now redundant, keep it anyway as a backstop, and move your effort to whatever the new default still doesn't reach. The day the classifier is uncertain about your intent, or the command isn't a git command at all, the only thing standing between you and lost work is the deterministic line you wrote back when you didn't trust anything to catch it for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The auto-mode safety details (the blocked git and IaC commands, the "didn't ask to discard" condition, the amend rule) are from the official Claude Code changelog, June 19, 2026: &lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;https://code.claude.com/docs/en/changelog&lt;/a&gt; . The near-miss is mine. Behavior and version specifics change quickly, so confirm against the current changelog before relying on any of it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>security</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I trusted my CLAUDE.md. WordPress.org rejected the exact thing it was supposed to prevent.</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Tue, 23 Jun 2026 23:46:08 +0000</pubDate>
      <link>https://dev.to/rapls/i-trusted-my-claudemd-wordpressorg-rejected-the-exact-thing-it-was-supposed-to-prevent-o1g</link>
      <guid>https://dev.to/rapls/i-trusted-my-claudemd-wordpressorg-rejected-the-exact-thing-it-was-supposed-to-prevent-o1g</guid>
      <description>&lt;p&gt;My CLAUDE.md had a rule about it. The generated code broke the rule anyway. And the thing that finally caught it wasn't my config file. It was a rejection from the WordPress.org plugin review team.&lt;/p&gt;

&lt;p&gt;I'd been building plugins with a long CLAUDE.md. Over time I'd folded two whole guides into it: the WordPress Japanese translation style rules I follow, and my own plugin design conventions, including the things you're specifically not supposed to do. It got long. And the longer it got, the safer I felt. It's written down, so it'll be followed. That was the assumption, and I never once questioned it.&lt;/p&gt;

&lt;p&gt;Then I shipped a plugin with trialware-shaped code around how paid features were gated, the kind of thing the WordPress.org directory guidelines push back on, and the kind of thing my own design guide explicitly told the agent to avoid. The rule was right there in the file. The generated code stepped on it anyway, and I didn't notice until the rejection came back.&lt;/p&gt;

&lt;p&gt;That rejection did something useful. It turned my trust in the config file inside out. I had written the rule. I had never checked whether it was in effect. And the thing that finally graded whether my own guide was working wasn't my guide. It was a reviewer on the other side, weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Written is not the same as in effect
&lt;/h2&gt;

&lt;p&gt;Here's the assumption I think most of us run on: a config file is something you write and then trust. Written equals followed. A long instruction file doesn't actually work that way, though. Its effect starts leaking the moment it gets long. There's good writing on this now: a monolithic CLAUDE.md that runs to a few thousand lines creates two problems at once. The agent processes all of it regardless of the task, and nobody fully owns it, so sections accumulate debt until it becomes the file everyone references and no one really trusts. I'd read exactly that, nodded, and kept adding lines anyway, because adding is easy and nothing ever tells you to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways it stops being followed, neither of them loud
&lt;/h2&gt;

&lt;p&gt;A config rule can go dead in two ways, and both are silent. The first is length. The rule is in there, it gets read, and it still doesn't shape the output, because it's one line competing with a couple thousand others for the model's attention. The second is drift: your conventions move on, but the file keeps steering the agent by the old ones, so it generates yesterday's practice with complete confidence. Both look identical from where you sit. The rule is written. You can point straight at it. And it isn't doing anything.&lt;/p&gt;

&lt;p&gt;That's the part I had backwards. I thought a careful enough config file was something I could trust. It can't be, because a config file can't certify its own effect. The proof that a rule is live never comes from the file. It comes from outside it: a test, or a review that catches what slipped through. My CLAUDE.md couldn't tell me the trialware rule had quietly stopped mattering. Only the rejection could.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now instead of trusting it
&lt;/h2&gt;

&lt;p&gt;So I stopped trying to build a config file I could trust, and started trying to measure whether the one I have is still in effect.&lt;/p&gt;

&lt;p&gt;In practice that's a few small changes. I keep it shorter than feels comfortable, and I treat adding a line as a cost rather than a free safety net, because every line I add dilutes the ones already fighting for attention. I put a little metadata at the top, an owner and a last-reviewed date, less for the agent than for the version of me six months from now who needs to know whether a section has gone stale. And before I lean on a rule for anything that actually matters, like a directory guideline I can get rejected over, I open a fresh session and ask the agent to tell me which conventions it thinks are currently in effect. Then I read that list for the ones that have gone missing. None of this restores trust in the file. It just gives me a way to keep checking, which turns out to be the only thing that was ever really holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note to my next self
&lt;/h2&gt;

&lt;p&gt;A config file is not as loaded as it looks. The length that makes you feel safe is the same length that dilutes the one rule you're counting on, and you won't feel it happen. Before you add the next line, check whether the last one is still doing anything.&lt;/p&gt;

&lt;p&gt;A config file isn't something you write and then trust. It's only as loaded as your last check. Mine was lighter than I thought, and it took a stranger's review to tell me.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Background reading on the monolithic-file problem, context drift, and config-file metadata: Packmind's 2026 context-engineering guides. The experience and the rejection are my own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>llm</category>
      <category>claude</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI found 300 WordPress plugin zero-days in 72 hours. I build plugins. Here's what changed for me.</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Mon, 22 Jun 2026 22:47:18 +0000</pubDate>
      <link>https://dev.to/rapls/ai-found-300-wordpress-plugin-zero-days-in-72-hours-i-build-plugins-heres-what-changed-for-me-43na</link>
      <guid>https://dev.to/rapls/ai-found-300-wordpress-plugin-zero-days-in-72-hours-i-build-plugins-heres-what-changed-for-me-43na</guid>
      <description>&lt;p&gt;Before I released my own AI chatbot plugin, I ran it through a security review. It came back with 35 bugs, three of them critical, and the one that made my stomach drop was an HTML injection coming straight out of unsanitized model output. At the time, that felt like my low point as a developer. Then I read this year's ecosystem numbers, and 35 started to look quaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers got loud in 2026
&lt;/h2&gt;

&lt;p&gt;A pipeline built by security researchers, reported by Help Net Security, paired AI static analysis with automated verification and surfaced more than 300 critical zero-days across the WordPress plugin ecosystem in about 72 hours of scanning, with every finding manually verified before disclosure. Patchstack's 2026 report puts a name on one of the causes: vibe coding, where developers ship LLM-generated plugin code they can't actually audit. One agency reported finding 100 distinct security issues in a single vibe-coded plugin.&lt;/p&gt;

&lt;p&gt;AI moved both sides of the board at once. It writes plugins fast, and while it's writing it skips the boring security parts: escaping, capability checks, nonce validation. Then it finds those exact holes fast, including on the attacker's side. The two things that used to protect a small plugin, obscurity and time, are both gone. Patchstack measured the weighted-median time from public disclosure to mass exploitation at roughly five hours. The standard advice, keep your plugins updated, assumes you have a window to react. Five hours is not a window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why my own plugin had those 35 bugs
&lt;/h2&gt;

&lt;p&gt;This is the part I think solo authors underrate, and it's the same thing that bit me. AI-written code gets trusted twice. Once because the AI wrote it, so it's probably fine. And again inside the code, where model output gets treated as safe and processed without a check. Both of those trusts were wrong in my codebase.&lt;/p&gt;

&lt;p&gt;My output-side HTML injection was that double-trust made concrete. I rendered the model's response straight into the page as HTML because I had quietly assumed that since the model generated it, it was clean. It wasn't. Model output carries other people's content inside it: whatever the user typed, whatever a retrieval step pulled off an external page. Treat that as safe and no amount of input-side guarding saves you. It leaks on the way out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually changed as a one-person shop
&lt;/h2&gt;

&lt;p&gt;I stopped treating "it runs" as "it's safe." I now read every AI-written handler by hand in three places: input, output, and permissions.&lt;/p&gt;

&lt;p&gt;On output, I treat the model's response as untrusted input and neutralize it for wherever it's going. Escape for HTML, allowlist for Markdown, validate any URL before fetching it. In WordPress terms that's the unglamorous stuff the model loves to skip: &lt;code&gt;esc_html&lt;/code&gt;, &lt;code&gt;wp_kses&lt;/code&gt; with a tight allowlist, &lt;code&gt;current_user_can&lt;/code&gt; and a nonce check at every AJAX and REST entry point, &lt;code&gt;$wpdb-&amp;gt;prepare&lt;/code&gt; on every write. None of it is new. It's the web security we've always done, pointed at the half of the code I didn't write myself.&lt;/p&gt;

&lt;p&gt;And the surface keeps growing. WordPress 7.0's Abilities API lets plugins expose actions to AI agents in a standard way, which is useful and is also a fresh place for under-scoped permissions to leak. That one I'm watching closely, because a plugin that hands an agent more power than it should is the next version of this same mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part isn't the code
&lt;/h2&gt;

&lt;p&gt;Here's where I think the 2026 conversation is actually pointing, and it's not a code problem. Patchstack found that 52 percent of plugin developers don't ship a patch before the vulnerability goes public, and that 46 percent of disclosed vulnerabilities had no fix available at all at the moment of disclosure.&lt;/p&gt;

&lt;p&gt;So finding bugs is no longer the bottleneck. AI does that in seconds. The bottleneck is everything after. Most plugins are free, maintained by one person between paying jobs, and a plugin earning zero revenue can't justify the cost of a fast security patch. The ecosystem's failure mode in 2026 isn't that bugs are hard to find. It's that the people who would fix them aren't paid to. AI didn't create that gap. It just made it visible at scale and handed attackers a five-hour head start.&lt;/p&gt;

&lt;p&gt;That reframes what "responsible" means for someone like me. Writing more carefully is necessary and nowhere near sufficient, because careful or not, the holes I miss are now findable in seconds by someone who isn't on my side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deadline nobody's talking about enough
&lt;/h2&gt;

&lt;p&gt;There's a clock on this too. By September 2026, plugin and theme developers distributing to EU users are required by law to have a vulnerability disclosure program. For a solo author that sounds like overhead, but it's the one structural fix that matches the threat: a real channel for someone to report the bug AI found, quietly, before it becomes a public CVE with a five-hour timer attached. Standing one up doesn't have to be heavy. For a solo author, the minimal version is a security contact in the plugin readme or a SECURITY file, a place for reports to land that isn't the public issue tracker, and a stated response window so the reporter knows the message won't sit unread. The point isn't ceremony. It's that the person who finds the bug has somewhere to send it before it turns into a public CVE with a five-hour timer attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note to my next self
&lt;/h2&gt;

&lt;p&gt;The 35 bugs taught me to distrust the code I didn't write. This year taught me the rest of it. The window to fix what I miss is shorter than it has ever been, and obscurity was never protecting me in the first place.&lt;/p&gt;

&lt;p&gt;If you ship plugins, AI-assisted or not, the move isn't to write more carefully and hope. It's to assume the holes are already findable in seconds, and to build the parts that catch them first: the hand review of input, output, and permissions, the output sanitization, and a disclosure channel that lets a friendly stranger reach you before an unfriendly one does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sources: Patchstack's 2026 State of WordPress Security report (vibe coding, the five-hour mass-exploitation median, the 52% and 46% patch figures, and the EU disclosure-program requirement). The 300-plus zero-days in 72 hours pipeline was reported by Help Net Security. The single-plugin "100 issues" figure and the WordPress 7.0 Abilities API note come from hosting.com's 2026 plugin-security writeup. Please confirm the latest figures against the primary reports before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>GitHub Copilot is usage-based now. Here's what that changes for terminal users.</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Mon, 22 Jun 2026 03:14:44 +0000</pubDate>
      <link>https://dev.to/rapls/github-copilot-is-usage-based-now-heres-what-that-changes-for-terminal-users-3c2p</link>
      <guid>https://dev.to/rapls/github-copilot-is-usage-based-now-heres-what-that-changes-for-terminal-users-3c2p</guid>
      <description>&lt;p&gt;As of June 1, 2026, all GitHub Copilot plans run on usage-based billing. Premium request units are gone. What replaced them is a token-metered currency called GitHub AI Credits: one credit equals one cent, and every model interaction converts into credits based on the input, output, and cached tokens it consumes, charged at each model's published rate.&lt;/p&gt;

&lt;p&gt;GitHub's framing is that Copilot outgrew its old pricing. A one-line completion and a multi-hour autonomous run used to cost the same, and once agentic use went mainstream, that flat rate stopped matching the compute behind it. Tying the price to tokens fixes the mismatch.&lt;/p&gt;

&lt;p&gt;If your Copilot use is mostly autocomplete, this barely registers. If you drive Copilot as an agent from the terminal, it changes which moves cost money. Here's the practical shape of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requests out, tokens in
&lt;/h2&gt;

&lt;p&gt;Old model: each interaction cost one premium request, scaled by a per-model multiplier, drawn from a monthly request allowance.&lt;/p&gt;

&lt;p&gt;New model: each interaction costs whatever its tokens cost on the model you picked. Every paid plan still ships with a monthly pool, now denominated in credits, with the option to set a budget for usage past it. Published figures put the included pool at 1,500 credits for Pro, 7,000 for Pro+, and 20,000 for Max, with pooled per-user allowances on Business and Enterprise.&lt;/p&gt;

&lt;p&gt;Worth knowing if you pay yearly: annual Pro and Pro+ subscribers stay on the request-based model until the term ends, and several model multipliers went up for them on June 1. An annual plan doesn't dodge the change. It postpones part of it while making the strong models eat more of the old allowance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autocomplete is untouched
&lt;/h2&gt;

&lt;p&gt;Before anyone starts rationing, here's the part that didn't move.&lt;/p&gt;

&lt;p&gt;Inline completions and Next Edit Suggestions are still unlimited and still free. If your day is mostly tab-completion in the editor, your costs read identical to May. Nothing to monitor there.&lt;/p&gt;

&lt;p&gt;The meter lands on the rest: chat, and especially the agentic runs that open files, plan, run commands, and iterate. That surface is the one that ballooned, and it's the one the new pricing is built around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The terminal is where it bites
&lt;/h2&gt;

&lt;p&gt;In the CLI you decide which model answers. Until June, that was a quality call and nothing else. Reaching for the strongest model on a trivial task cost nothing you could see.&lt;/p&gt;

&lt;p&gt;Now the per-token prices sit behind that choice, and the gap between models is large. One analysis of GitHub's published rates pointed out that output on the priciest listed model runs many times the cheapest, wide enough that grabbing a heavyweight model by reflex is basically a billing bug. Spinning one up to reflow a paragraph isn't just wasteful anymore. It's billable waste.&lt;/p&gt;

&lt;p&gt;So for agentic and CLI-heavy work, two ordinary habits become cost decisions: the model you default to, and how long you let a session run. A sprawling all-day agent session now carries a price tied to its length, not just its result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "heavy" costs
&lt;/h2&gt;

&lt;p&gt;The included pools feel roomy until you hold them against real agent use. On the GitHub community thread, a Pro+ user reported spending around 360 credits in one ordinary development day and projected a normal month running well past the included 7,000.&lt;/p&gt;

&lt;p&gt;That's one workload, not a benchmark, and how near you get depends entirely on how agentic your days are. The signal isn't the number, it's the shape: under the new system, an ordinary day has a credit cost, and for agent-leaning work that cost can rise faster than the monthly pool implies.&lt;/p&gt;

&lt;h2&gt;
  
  
  New guardrails, and a double charge
&lt;/h2&gt;

&lt;p&gt;GitHub shipped budget controls with the switch, including a hard stop at your limit and a usage view that shows credits draining over the month. There's also a pay-as-you-go path, so you can keep working on your current plan after the included credits run dry instead of being pushed to upgrade.&lt;/p&gt;

&lt;p&gt;The wrinkle teams should flag: Copilot code review moved to an agentic architecture, and as of June 1 it consumes GitHub Actions minutes on private repositories on top of AI Credits. A single pull request review can now hit two parts of the bill, the model cost and the runner minutes. Public repos keep their free Actions minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The open question
&lt;/h2&gt;

&lt;p&gt;Cheaper or pricier isn't one answer. For autocomplete-first users it's about flat. For agent-first CLI users the bill now tracks how hard they run sessions and which models they reach for, which can land either way depending on discipline.&lt;/p&gt;

&lt;p&gt;Two things to keep an eye on. The price spread rewards matching the model to the task, so a deliberate default plus selective escalation is now a cost strategy, not only a quality one. And the budget hard stop is the cleanest defense against a surprising month, the kind of setting that's better on before you need it than after.&lt;/p&gt;

&lt;p&gt;The pattern underneath is the one GitHub says out loud: agentic coding has cloud economics attached now. The terminal feels it first, because the terminal is where the long, autonomous, token-hungry sessions actually run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sources: GitHub's announcement "GitHub Copilot is moving to usage-based billing" and the June 1 changelog "Updates to GitHub Copilot billing and plans" (github.blog). Included-credit figures from GitHub Docs; the per-day usage figure from GitHub Community discussion #192948; model-rate analysis from published Copilot per-token rates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>github</category>
      <category>githubcopilot</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Connecting an MCP server gives your agent hands. It also gives a stranger a way in.</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Sun, 21 Jun 2026 01:53:52 +0000</pubDate>
      <link>https://dev.to/rapls/connecting-an-mcp-server-gives-your-agent-hands-it-also-gives-a-stranger-a-way-in-3mgi</link>
      <guid>https://dev.to/rapls/connecting-an-mcp-server-gives-your-agent-hands-it-also-gives-a-stranger-a-way-in-3mgi</guid>
      <description>&lt;p&gt;The moment you connect an MCP server, your coding agent stops being a thing that reads and writes in your repo and becomes a thing that can reach out and act. Read a database, hit an API, touch a service, pull in a web page. That's the entire appeal. It's also the entire problem, and the two are the same feature seen from two sides.&lt;/p&gt;

&lt;p&gt;I went through this wiring up tools for my own plugin work, and the thing that saved me from a worse mistake was a scar I already had. I'd shipped an AI chatbot earlier where I rendered the model's output straight to the page and ate an HTML injection bug. That one taught me a rule the hard way: anything an LLM hands back is untrusted input. MCP is that same lesson with the blast radius turned up, because now the untrusted thing isn't just my model's text, it's whatever a connected server decides to return.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different fears, and people only have one
&lt;/h2&gt;

&lt;p&gt;When people talk about agent safety, they almost always mean: what if the agent runs something destructive. Deletes files, force-pushes, curls something it shouldn't. That fear is real and it has a real answer, which I'll get to.&lt;/p&gt;

&lt;p&gt;But it's only half the threat, and it's the visible half. The other half is quieter: what if the agent believes something it shouldn't. An MCP server returns an API response, a database row, an issue body, an email thread, and somewhere in that returned content is a line that reads like an instruction. The model has no reliable way to tell your instruction from text that arrived inside data. To it, they're the same channel. So the danger isn't only the command the agent runs, it's the command it gets talked into running by content that came back through a tool.&lt;/p&gt;

&lt;p&gt;Those are two separate problems, and they need two separate defenses. Most setups install one and assume they're covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output side: treat every tool return as a form field
&lt;/h2&gt;

&lt;p&gt;Here's the rule I carry over from the chatbot bug. Whatever an MCP server returns is in exactly the same trust category as a string a stranger typed into a form on your site. Not "data from my tool." Data from outside, that happens to arrive through my tool.&lt;/p&gt;

&lt;p&gt;That reframe changes what you do with it. You don't pass a tool's raw output straight into the arguments of the next command. You don't render it to a screen without escaping. And you don't let an imperative sentence buried in a returned document quietly become something the agent treats as a directive. The output is a payload to inspect, not a value to trust, and the fact that it came from a server you connected doesn't launder it. You connected the pipe. You didn't vouch for everything that flows through it.&lt;/p&gt;

&lt;p&gt;This is the half the tooling won't do for you, because it can't. No sandbox flag knows whether a returned string is a legitimate API result or a planted instruction. That judgment lives in how you wire the data, not in a setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The action side: walls, not manners
&lt;/h2&gt;

&lt;p&gt;The other half, the destructive-command fear, does have a settings answer, and it's worth getting exactly right because it's easy to half-do.&lt;/p&gt;

&lt;p&gt;Asking the model nicely not to run dangerous things is manners, and manners get bypassed the moment the input talks it into something. What you want is a wall. In Claude Code that's the sandbox: turn it on and Bash execution gets isolated at the OS level, with the restriction inherited by every subprocess the agent spawns. Seatbelt on macOS, Bubblewrap on Linux and WSL2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowUnsandboxedCommands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the part that surprised me, straight from the docs: the sandbox restricts writes to your working directory, but by default it still allows reads across most of the machine. Which means credentials like &lt;code&gt;~/.aws/credentials&lt;/code&gt; and &lt;code&gt;~/.ssh/&lt;/code&gt; are readable unless you say otherwise. The sandbox is a wall around writing and executing, not around reading. Reading you close yourself, with deny rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(rm -rf *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(wget *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(~/.ssh/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(~/.aws/**)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the combination that actually matters, and it's exactly where the two halves meet. If a tool return talks the agent into exfiltrating a secret, the read-deny is what stops it from reading the secret, and the network-command deny is what stops it from sending it. The output-side problem is what gets the agent to try; the action-side walls are what make the attempt fail. Neither covers for the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third thing: don't connect what you don't trust, or don't need
&lt;/h2&gt;

&lt;p&gt;The server itself is attack surface. A connected MCP server you didn't vet is code running in your loop with a channel into your agent. So three habits, none of them clever:&lt;/p&gt;

&lt;p&gt;Vet the source before you connect it. Who wrote it, can you see the code. An MCP server is not a browser tab you can close and forget; while it's connected, it's part of your trust boundary.&lt;/p&gt;

&lt;p&gt;Drop the ones you're not using. Every idle server is attack surface and context weight at once, and &lt;code&gt;/mcp&lt;/code&gt; will show you what's actually connected versus what you wired up once and forgot.&lt;/p&gt;

&lt;p&gt;And the one I reach for most: if a CLI already does the job, don't stand up a server at all. I drive WordPress with wp-cli, and rather than wrap it in an MCP server I call it from a Skill when I need it. One less always-connected surface, one less thing returning content I'd have to distrust. "Connect a server" and "call a command" are not the same risk, and the second is often the smaller one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually check now
&lt;/h2&gt;

&lt;p&gt;Before I wire up any MCP server, three questions, in this order. Do I trust where this server came from. What's the worst command it could talk my agent into, and is that command walled off by deny rules. And is the output going to get treated as untrusted input everywhere it lands, or am I about to pass a stranger's text straight into a sink.&lt;/p&gt;

&lt;p&gt;The sandbox answers exactly one of those. The other two are on me, and they're the ones that bit me first. Connecting a server is genuinely the moment your agent grows hands. Worth remembering that hands can be guided by whoever's holding the other end of the tool.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build WordPress plugins and write about AI tooling and security at &lt;a href="https://raplsworks.com/" rel="noopener noreferrer"&gt;https://raplsworks.com/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>claudecode</category>
      <category>mcp</category>
    </item>
    <item>
      <title>I Created a Gemini API Key and Got AQ. Instead of AIza</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Sat, 20 Jun 2026 02:13:04 +0000</pubDate>
      <link>https://dev.to/rapls/i-created-a-gemini-api-key-and-got-aq-instead-of-aiza-3dp3</link>
      <guid>https://dev.to/rapls/i-created-a-gemini-api-key-and-got-aq-instead-of-aiza-3dp3</guid>
      <description>&lt;p&gt;I created a free Gemini API key in Google AI Studio. When I went to copy it, I stopped. It started with &lt;code&gt;AQ.Ab&lt;/code&gt;. Not the &lt;code&gt;AIza&lt;/code&gt; I had been looking at for as long as I can remember.&lt;/p&gt;

&lt;p&gt;My first thought was that something had gone wrong with my account. A leaked key, or a restriction, or some flag on the project. I looked into it, and it was none of that. Google is partway through changing how API keys are issued, and a freshly created key now comes out in the new format. Nothing broke. This is the plan.&lt;/p&gt;

&lt;p&gt;That said, you can't drop the new key into every path and expect it to work. Some routes reject it. I run a WordPress plugin called Rapls AI Chatbot, and one of its no-card onboarding paths uses the Gemini free tier, so the first thing I did was check what was affected. Here is the short version before the details. The new &lt;code&gt;AQ.&lt;/code&gt; key works fine on the native Gemini endpoint. It gets rejected on OpenAI-compatible endpoints and by some third-party tools that assume the &lt;code&gt;AIza&lt;/code&gt; format.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Checked: June 2026&lt;br&gt;
Issued from: Google AI Studio (free tier)&lt;br&gt;
Routes tested: native &lt;code&gt;generativelanguage.googleapis.com&lt;/code&gt; and OpenAI-compatible &lt;code&gt;/v1beta/openai&lt;/code&gt;&lt;br&gt;
Reference: Google docs, "Using Gemini API keys" &lt;a href="https://ai.google.dev/gemini-api/docs/api-key" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/api-key&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From AIza to AQ., what is actually happening
&lt;/h2&gt;

&lt;p&gt;Google is moving Gemini API keys from the old Standard key (&lt;code&gt;AIza...&lt;/code&gt;) to a new Auth key (&lt;code&gt;AQ.Ab...&lt;/code&gt;). Keys you create in AI Studio now come out as Auth keys automatically. So someone who saw &lt;code&gt;AIza&lt;/code&gt; yesterday gets &lt;code&gt;AQ.&lt;/code&gt; today. That is what happened on my end.&lt;/p&gt;

&lt;p&gt;There are dates attached, and this was the part that mattered most. Starting June 19, 2026, the Gemini API began rejecting requests from unrestricted Standard keys. By the time I am writing this, that deadline has already passed. Then in September 2026, Standard keys get rejected outright. Restricted or not, an &lt;code&gt;AIza&lt;/code&gt; Standard key stops working after September.&lt;/p&gt;

&lt;p&gt;One thing worth adding. A Standard key with an explicit restriction keeps working past June 19. So it is not that every &lt;code&gt;AIza&lt;/code&gt; key died on the 19th. The unprotected ones go first, and the rest follow. It is a staged shutdown.&lt;/p&gt;

&lt;p&gt;Auth keys behave a little differently in one place. Requests authenticated with an Auth key do not show up in Google Cloud's service account usage metrics. The project and key live on the AI Studio side. If you have been watching requests in the Cloud Console, your numbers may stop adding up, and that is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap I hit was compatibility, not the key itself
&lt;/h2&gt;

&lt;p&gt;The format change matters less than the compatibility fallout. The &lt;code&gt;AQ.&lt;/code&gt; key has a few known snags. Here is what I saw in my own testing alongside what people are reporting on the official forum.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;How the key is passed&lt;/th&gt;
&lt;th&gt;What the &lt;code&gt;AQ.&lt;/code&gt; key does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native Gemini&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;?key=&lt;/code&gt; query or &lt;code&gt;x-goog-api-key&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;Works. curl returns 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI-compatible&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Authorization: Bearer&lt;/code&gt; to &lt;code&gt;/v1beta/openai/...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;400 with &lt;code&gt;Multiple authentication credentials received&lt;/code&gt;. 401 in some implementations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AIza-assuming third party&lt;/td&gt;
&lt;td&gt;Tool expects the &lt;code&gt;AIza&lt;/code&gt; format internally&lt;/td&gt;
&lt;td&gt;&lt;code&gt;401 invalid_api_key&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Native is fine. If you hit &lt;code&gt;generativelanguage.googleapis.com&lt;/code&gt; directly and pass the key as a query param or header, &lt;code&gt;AQ.&lt;/code&gt; goes through the same as before. The official SDKs (&lt;code&gt;google-genai&lt;/code&gt; and friends) handle that internally, so going through an SDK you probably will not even notice.&lt;/p&gt;

&lt;p&gt;The OpenAI-compatible path is where it jams. Send an &lt;code&gt;AQ.&lt;/code&gt; key with &lt;code&gt;Authorization: Bearer&lt;/code&gt; to an OpenAI-compatible endpoint and it can come back with &lt;code&gt;Multiple authentication credentials received&lt;/code&gt;, as if you passed credentials twice. In the same family of reports, a key that returns 200 through the native route still comes back as &lt;code&gt;401 invalid_api_key&lt;/code&gt; once a tool quietly routes it through an OpenAI-compatible transport. The nasty part of that second case is that the key is alive, but the error says the key is invalid. Trust the message, blame the key, and you never get out of the loop.&lt;/p&gt;

&lt;p&gt;Some desktop tools that assume the &lt;code&gt;AIza&lt;/code&gt; format throw the same 401.&lt;/p&gt;

&lt;p&gt;In my case, the plugin talks to Gemini natively, so the &lt;code&gt;AQ.&lt;/code&gt; key sailed through. To be safe I ran a new &lt;code&gt;AQ.&lt;/code&gt; key through a full round trip, and it sent and received without trouble. I am folding that confirmation into the plugin's guidance in an update going out the next day. If I had been routing through an OpenAI-compatible path, users would have hit a 401 the moment they pasted a new key. With something you ship, your code being correct does not help. What piles up is a stack of "I entered the key and it will not work" reports. I was glad I checked first.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are still on an AIza key
&lt;/h2&gt;

&lt;p&gt;This part I will spell out. If you are running on a key that starts with &lt;code&gt;AIza&lt;/code&gt; and it still works, that is not a reason to relax. The dates above are why.&lt;/p&gt;

&lt;p&gt;Check whether the key is restricted. In the AI Studio key list, if a key carries the &lt;code&gt;Unrestricted&lt;/code&gt; label, that is the kind that stops after June 19. To keep it alive, restrict it. Hover the &lt;code&gt;Unrestricted&lt;/code&gt; label on the API Keys page, click Add restrictions in the dialog, choose Restrict to Gemini API only, and confirm with Restrict key. Now it is locked to the Gemini API and keeps running past June 19.&lt;/p&gt;

&lt;p&gt;This restriction only buys time. September pulls in Standard keys as a whole, so even a restricted &lt;code&gt;AIza&lt;/code&gt; key eventually stops. Treat it as breathing room for an unhurried migration, nothing more. The destination is the &lt;code&gt;AQ.&lt;/code&gt; Auth key.&lt;/p&gt;

&lt;p&gt;Restricting a key needs the &lt;code&gt;apikeys.keys.update&lt;/code&gt; permission on the relevant Google Cloud project, which the API Keys Admin and Editor roles already include. If your key is shared across other Google APIs (not a setup you want), you restrict it from the Cloud Console rather than AI Studio.&lt;/p&gt;

&lt;p&gt;For what it is worth, the forum also has a separate thread about accounts getting restricted and no longer being able to create &lt;code&gt;AIza&lt;/code&gt; keys at all. The &lt;code&gt;AQ.&lt;/code&gt; issuance here is unrelated. It is the format change that applies to everyone. A new key coming out as &lt;code&gt;AQ.&lt;/code&gt; does not mean something is wrong with your account.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line of action
&lt;/h2&gt;

&lt;p&gt;Put together, there is not much to verify on your end. If you are using a new &lt;code&gt;AQ.&lt;/code&gt; key, confirm your traffic goes to the native Gemini endpoint. If you run an OpenAI-compatible path, test on real hardware whether &lt;code&gt;Authorization: Bearer&lt;/code&gt; accepts the &lt;code&gt;AQ.&lt;/code&gt; key. If you are still on an &lt;code&gt;AIza&lt;/code&gt; key, restrict it to buy time and move to &lt;code&gt;AQ.&lt;/code&gt; before September. That is the whole list.&lt;/p&gt;

&lt;p&gt;Back to where I started. Staring at &lt;code&gt;AQ.Ab&lt;/code&gt; in AI Studio with my hand frozen over the copy button, I suspected the key. What I actually had to fix was not the key. It was how I was handing the key to each route. If you froze at the same screen, point your suspicion at the route first.&lt;/p&gt;

&lt;p&gt;Reference: Google AI for Developers, "Using Gemini API keys" &lt;a href="https://ai.google.dev/gemini-api/docs/api-key" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/api-key&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>api</category>
      <category>webdev</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>I automated everything except the code, and that's where Claude Code actually paid off</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Fri, 19 Jun 2026 23:54:19 +0000</pubDate>
      <link>https://dev.to/rapls/i-automated-everything-except-the-code-and-thats-where-claude-code-actually-paid-off-51oh</link>
      <guid>https://dev.to/rapls/i-automated-everything-except-the-code-and-thats-where-claude-code-actually-paid-off-51oh</guid>
      <description>&lt;p&gt;I build WordPress plugins on my own, and I started using Claude Code for the obvious reason: to write code faster. Six months later, the part it actually saved me wasn't the code at all. It was everything around the code.&lt;/p&gt;

&lt;p&gt;Every release, I ran the same small gauntlet. Bump the version, update the readme, write the changelog, check the translation files for gaps, draft the announcement. None of it is hard. All of it is fiddly, and every one of those chores pulled me out of the part of my brain that was actually building the thing. By the time I'd finished the release ritual and came back to code, I had to reload the whole mental context I'd just dropped.&lt;/p&gt;

&lt;p&gt;That reload is the real cost, and it took me a while to see it. The chores don't just eat their own time. They evict the code from your head, and you pay again to bring it back. So I started handing the chores to Claude Code instead of the code, and that was the version that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chore that breaks releases silently
&lt;/h2&gt;

&lt;p&gt;The one I least wanted to get wrong is version mismatch. In a WordPress plugin, the displayed version comes from the &lt;code&gt;Version&lt;/code&gt; header in the main PHP file, but the readme's &lt;code&gt;Stable tag&lt;/code&gt; points at which tagged version is "stable." They are two separate fields with two separate jobs, and when they disagree, updates can reach users wrong. No error fires. You find out from a bug report.&lt;/p&gt;

&lt;p&gt;This is a perfect chore to hand off, because it's pure checking, no judgment. I keep the steps in a file and have Claude Code run them before every release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;does the main file &lt;code&gt;Version&lt;/code&gt; match the intended release&lt;/li&gt;
&lt;li&gt;does the readme &lt;code&gt;Stable tag&lt;/code&gt; point at the right thing&lt;/li&gt;
&lt;li&gt;is the top changelog entry actually this version&lt;/li&gt;
&lt;li&gt;is &lt;code&gt;Tested up to&lt;/code&gt; tracking the current stable WordPress, and not set higher than it should be&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important half is the instruction I attach to it: surface mismatches, don't fix them. Show me the file and line, and let me make the call. I don't want the convenience of auto-correction on the one step where a wrong guess ships to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  I stopped writing the prompt and made it a command
&lt;/h2&gt;

&lt;p&gt;The version check is identical every single time, so writing the prompt by hand each release was its own small chore. I moved it into a reusable command.&lt;/p&gt;

&lt;p&gt;Worth knowing if you're on a recent Claude Code: custom slash commands got merged into Skills back in the April update. The old &lt;code&gt;.claude/commands/&lt;/code&gt; files still work, but the current home is &lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;, which gets you the same &lt;code&gt;/name&lt;/code&gt; invocation plus the option of Claude calling it on its own when the context fits.&lt;/p&gt;

&lt;p&gt;Mine looks roughly like this:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release-check&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WordPress&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;plugin's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;readme&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;consistency&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;release.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shipping."&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Bash(grep:*)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Release check&lt;/span&gt;

Verify before release:
&lt;span class="p"&gt;1.&lt;/span&gt; main PHP Version header vs readme Stable tag
&lt;span class="p"&gt;2.&lt;/span&gt; top changelog entry is this version
&lt;span class="p"&gt;3.&lt;/span&gt; Tested up to tracks current stable, not above RC

Report mismatches as file + line. Do not edit. The human decides.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things earn their place here. The &lt;code&gt;allowed-tools&lt;/code&gt; line scopes it to reading and grep, so a check can't quietly turn into an edit. And the &lt;code&gt;description&lt;/code&gt; says when to use it in plain terms, which is what lets Claude reach for it at the right moment instead of guessing. Now the whole thing is &lt;code&gt;/release-check&lt;/code&gt; and I never retype it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning developer noise into user-facing notes
&lt;/h2&gt;

&lt;p&gt;The other recurring time sink is the readme and changelog. WordPress readmes use their own markdown subset with a fixed shape, so turning a pile of commits into the right section is well-suited to a model, as long as you keep it honest.&lt;/p&gt;

&lt;p&gt;I hand it the commit range and ask for a draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; v1.9.2..HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then: rewrite these as a user-facing changelog, keep the format, and where a number or detail isn't certain, write &lt;code&gt;(to confirm)&lt;/code&gt; instead of guessing. That last rule matters more than it looks. Left alone, a model fills every gap smoothly, and smooth is exactly what hides the spot you needed to check. Forcing it to leave the uncertain parts visibly blank means I catch them.&lt;/p&gt;

&lt;p&gt;The translation step works the same way: I have it diff the &lt;code&gt;.pot&lt;/code&gt; against each &lt;code&gt;.po&lt;/code&gt; and list only the untranslated strings, no translating yet. See the gap first, decide second. Same shape as the version check: show me the state, don't act on it.&lt;/p&gt;

&lt;p&gt;One small landmine I hit once: WordPress readmes don't render tables, and if the file grows past about 10KB the parser starts misbehaving. So when the changelog gets long, I have Claude move the old entries into a separate &lt;code&gt;changelog.txt&lt;/code&gt;, as a pure move, no rewriting. The rule there is explicit: relocate, don't summarize, or you lose the history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I draw the line
&lt;/h2&gt;

&lt;p&gt;I keep all of this at half-automation on purpose. Three things stay manual: hitting the release button, the wording that users actually see, and merging code. Everything Claude produces arrives as a draft or a check result, never as a done deal.&lt;/p&gt;

&lt;p&gt;That isn't caution for its own sake. Hand the whole thing over and you end up shipping text to users that nobody read first, and for a plugin that's a trust problem, not a time problem. The &lt;code&gt;allowed-tools&lt;/code&gt; scoping and the forced &lt;code&gt;(to confirm)&lt;/code&gt; markers are both just mechanisms for holding that line: let the machine do the fetching and the diffing, keep the judgment with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't expect
&lt;/h2&gt;

&lt;p&gt;I went in wanting Claude Code to make the coding faster. What it actually did was give me the coding back, by clearing the rubble around it.&lt;/p&gt;

&lt;p&gt;The chores were never the expensive part on their own. The expensive part was what they did to focus: each small task knocked the code out of my head, and the reload was the tax. Moving the chores off my plate didn't just save their minutes. It let me stay in the build instead of climbing back into it five times a release. If you're reaching for an AI agent to write more code, it's worth checking whether the thing actually slowing you down is the code at all, or the ring of small jobs around it that keep breaking your attention. For me it was the ring, and that's the part worth handing off first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build WordPress plugins and write about AI tooling and security at &lt;a href="https://raplsworks.com/" rel="noopener noreferrer"&gt;https://raplsworks.com/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudcode</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Write your error states for a stranger three months from now, not for yourself today</title>
      <dc:creator>Rapls</dc:creator>
      <pubDate>Thu, 18 Jun 2026 23:40:13 +0000</pubDate>
      <link>https://dev.to/rapls/write-your-error-states-for-a-stranger-three-months-from-now-not-for-yourself-today-54jm</link>
      <guid>https://dev.to/rapls/write-your-error-states-for-a-stranger-three-months-from-now-not-for-yourself-today-54jm</guid>
      <description>&lt;p&gt;Most error messages are written for the wrong reader.&lt;/p&gt;

&lt;p&gt;They're written for the person who's watching when the thing breaks. You're at the terminal, the run fails, the message says &lt;code&gt;connection refused&lt;/code&gt; or &lt;code&gt;validation failed at step 3&lt;/code&gt;, and that's enough, because you have all the context in your head right now. You know what you were doing, what you changed, what the system was supposed to do. The message just has to jog a memory you already have.&lt;/p&gt;

&lt;p&gt;The reader who actually needs the error state is someone else entirely, and they show up months later. I got talked into this view in a long comment thread on an earlier post, and it changed how I think about failure handling, especially for the async, agent-driven work I do a lot of now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reader you're not designing for
&lt;/h2&gt;

&lt;p&gt;Here's the gap. When an interactive tool fails, there's a human in the loop who reacts immediately. The error can be terse because the context is live. You scroll up, you see the conversation, you fix it.&lt;/p&gt;

&lt;p&gt;Async work has none of that. An agent runs a pipeline at 2am, something fails partway, and nobody sees it land. There's no conversation to scroll back through, no replay button, no warm context in anyone's head. Whoever investigates shows up cold, hours or weeks later, and the only thing they have is whatever the process wrote down before it stopped.&lt;/p&gt;

&lt;p&gt;Which means, for that reader, the error state isn't a report about the failure. It is the failure, as far as they can ever see. If the record doesn't contain what they need to reconstruct what happened, the information is gone. Not hard to find. Gone.&lt;/p&gt;

&lt;p&gt;That reframes the whole design question. It stops being "how do we format this error" and becomes "what does an investigator need to rebuild this run from nothing." Those are different specs, and almost every error message I've ever written was quietly answering the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A message versus a record
&lt;/h2&gt;

&lt;p&gt;The cleanest way I can put the distinction: most errors are a message, and what async work needs is a record.&lt;/p&gt;

&lt;p&gt;A message is written for someone who shares your context. It can be short because it's pointing at things you both already know. "Validation failed" is a fine message when you're standing right there.&lt;/p&gt;

&lt;p&gt;A record is written for someone who has nothing. It has to carry the context with it, because there's no shared memory to lean on. What was the input. What stage was this. What did the system believe was true when it decided to proceed. What got retried, how many times, with what result. A record is heavier on purpose, because its whole job is to survive the gap between the failure and the person who reads about it.&lt;/p&gt;

&lt;p&gt;The test I use now: if I deleted the rest of the system and handed someone only this error state, could they tell me what went wrong? If the answer depends on context that lives anywhere other than the record itself, I'm writing a message and calling it a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap is also where organizations forget
&lt;/h2&gt;

&lt;p&gt;There's a second reason the later reader has nothing, and it's not just time. It's people.&lt;/p&gt;

&lt;p&gt;The person who built the system often isn't the person debugging it three months on. They've moved teams, moved companies, or just moved on to other work and dumped the context. So the investigator isn't even future-you, who at least shared your assumptions once. It's a stranger who was never in the room when the decisions got made.&lt;/p&gt;

&lt;p&gt;That's the strongest version of the spec, and the most honest one. You're not writing for yourself later. You're writing for someone who has none of your context and never did, and who is meeting your system for the first time through its failure. If the error state only makes sense to someone who already understands the system, it's useless to exactly the person most likely to be reading it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changes in practice
&lt;/h2&gt;

&lt;p&gt;I haven't rebuilt everything. But a few habits shifted, and they're cheap.&lt;/p&gt;

&lt;p&gt;I write error states as if the reader has no access to the rest of the run. Not "step 3 failed" but what step 3 was trying to do, what it received, and what it expected. The extra sentence costs nothing now and saves an hour later.&lt;/p&gt;

&lt;p&gt;I treat the clean error state as the audit trail I'm choosing to have, rather than a thing I bolt on if there's time. In async work there's no other trail. Either the failure left evidence or it didn't, and that's decided when I write the handler, not when the incident happens.&lt;/p&gt;

&lt;p&gt;And I stopped optimizing failure output for the demo. The version that looks good when you're watching it succeed is not the version that helps when it fails unattended. Those are different audiences, and the unattended one is the one that actually needs help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest version of the idea
&lt;/h2&gt;

&lt;p&gt;If I compress all of it into one line, it's this: useful right now and useful in three months are different specs, and you usually only get to satisfy one of them, so pick the harder reader.&lt;/p&gt;

&lt;p&gt;Pick the stranger. Write the record they'd need, not the message you'd understand. You won't be in the room when it's read, and the whole point of an error state is to work when you're not there.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build WordPress plugins and write about AI tooling and security at &lt;a href="https://raplsworks.com/" rel="noopener noreferrer"&gt;https://raplsworks.com/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The experiences and decisions in this post are my own. English isn't my first language, so I use an AI assistant to help draft and edit the writing.&lt;/p&gt;

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