DEV Community

Cover image for I thought Claude Code would make my work lighter. I became its checker instead. Here's every file I wrote to stop
Rapls
Rapls

Posted on

I thought Claude Code would make my work lighter. I became its checker instead. Here's every file I wrote to stop

I thought handing work to Claude Code would make my days lighter.

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.

What actually started was a different job.

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.

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.

What I wanted to hand off was checking, not writing

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.

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.

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.

1. CLAUDE.md: write the reason next to the rule

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.

Prime Cache saves pages as HTML files and serves them before WordPress loads. That means one file runs before WordPress exists:

### Important Constraints
- `dropins/page-cache.php` runs before WordPress — cannot use any WP functions (no `get_option()`, `wp_parse_url()`, etc.)
- Atomic writes (temp file + rename) prevent serving partial files under concurrency
- Path traversal protection via `realpath()` validation against cache directory
Enter fullscreen mode Exit fullscreen mode

Before I wrote this, Claude would sometimes helpfully reach for get_option() 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.

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.

Say what's banned, and what isn't

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:

Prohibited:
- Copying a function/method body and only renaming variables or constants
- Copying comment blocks or docblocks
- Directly mimicking multi-line code structure

Acceptable similarities:
- `wp_cache_*` signatures required by the WordPress drop-in API
- WordPress hook names (`transition_post_status`, `create_term`, ...)
- Checks for standard WordPress constants (`DONOTCACHEPAGE`, `WP_CACHE`, ...)
- Industry-standard patterns (atomic write, gzip, 304 responses)
Enter fullscreen mode Exit fullscreen mode

(The real file is in Japanese, and it names the plugins I'm studying.)

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.

The release zip, and "empty means pass"

This is the part I most want to share.

My CLAUDE.md has the zip command with its exclude list, and then a check that must be run afterwards:

unzip -Z1 prime-cache.zip | grep -iE "\.gitattributes|\.claude|/\.git/|\.serena|\.DS_Store|CLAUDE.md|/tests/|/docs/"
Enter fullscreen mode Exit fullscreen mode

It lists the files in the zip and looks for names that must not be there. CLAUDE.md says the result "must be empty."

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.

Two notes in that section exist because of real near-misses:

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

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.

Keep the places that broke before

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."

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.

2. AGENTS.md: a find-and-replace copy that broke silently

While writing this up, I compared files and found a mistake I should be honest about.

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":

-x "prime-cache/.Codex/*" \
-x "prime-cache/*/.Codex/*" \
-x "prime-cache/AGENTS.md" \
-x "prime-cache/**/AGENTS.md" \
Enter fullscreen mode Exit fullscreen mode

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

Worse, the check was replaced the same way:

unzip -Z1 prime-cache.zip | grep -iE "\.gitattributes|\.Codex|/\.git/|\.serena|\.DS_Store|AGENTS.md|..."
Enter fullscreen mode Exit fullscreen mode

It doesn't look for .claude or CLAUDE.md, so even if they were in the zip, the result would be empty. A pass.

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".

Two lessons:

  1. 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.
  2. 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 .claude/, once, and see whether it fails.

3. settings.json: stop it with mechanisms, not requests

CLAUDE.md is a request. Claude follows it almost every time. Almost.

What must never happen goes into ~/.claude/settings.json, in three boxes.

allow is what Claude can do without asking: read, search, edit, git status, git diff, 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.

ask has only four entries in my setup:

"ask": [
  "Bash(svn commit:*)",
  "Bash(svn ci:*)",
  "Bash(gh release create:*)",
  "Bash(git push --tags:*)"
]
Enter fullscreen mode Exit fullscreen mode

All of them lead to a release. svn commit 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.

deny is what must not happen:

"deny": [
  "Read(**/.env)",
  "Read(~/.ssh/**)",
  "Edit(~/.claude/settings.json)",
  "Edit(~/.claude/hooks/**)",
  "Bash(curl:*)",
  "Bash(wget:*)",
  "Bash(sudo:*)",
  "Bash(git reset --hard:*)",
  "Bash(git push --force:*)",
  "Bash(wp plugin delete:*)",
  "Bash(wp db drop:*)",
  "Bash(wp eval:*)"
]
Enter fullscreen mode Exit fullscreen mode

(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.

Sandbox: decide which roads are open

"sandbox": {
  "enabled": true,
  "failIfUnavailable": true,
  "network": {
    "allowedDomains": ["registry.npmjs.org", "packagist.org", "wordpress.org", "*.wordpress.org", "github.com"],
    "strictAllowlist": true
  }
}
Enter fullscreen mode Exit fullscreen mode

(The real list has a few more npm and Composer hosts.) failIfUnavailable 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.

Auto mode: tell it what's normal here

defaultMode is auto, where Claude judges for itself whether an action needs asking. I describe my environment for it, in plain sentences. Paraphrased:

  • Rapls Works is a one-person WordPress plugin business.
  • 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.
  • Stripe keys, Pro licenses, customer data, .env, and wp-config.php must never leave this machine or go into Git.
  • There's no staging environment. Some folders in the working tree are deliberately untracked; if they're deleted, Git can't bring them back.
  • Commits, branches, local merges and rebases, builds, tests, and pushing to my own GitHub are routine. No need to ask.

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.

4. A hook: never lose the folder twice

Hooks run my own script right before Claude Code executes a command. Mine starts like this:

# Blocks shell commands that could delete, move, or overwrite the Prime Cache
# plugin *development* folders (prime-cache / prime-cache-pro) or otherwise wipe
# the working tree. Added after the 2026-07-19 folder-loss incident.
Enter fullscreen mode Exit fullscreen mode

Yes. I lost it once.

deny already had dangerous commands, but there are many ways to write them. Block rm -rf and mv, rsync --delete, and find -delete are still open. So the hook blocks a command when it contains both a destructive operation and a dev folder name:

destructive='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'
if printf '%s' "$low" | grep -Eq "$destructive"; then
  if printf '%s' "$low" | grep -Eq "$target"; then
    deny "Blocked: this command could delete, move, or overwrite the Prime Cache plugin development folder ..."
  fi
fi
Enter fullscreen mode Exit fullscreen mode

Two design choices:

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

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.

5. Handovers: make the new session start itself

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:

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.
Enter fullscreen mode Exit fullscreen mode

/init 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.

6. Ask for evidence with "done"

The last piece is how "done" arrives. Here is what I'd add to the end of a CLAUDE.md:

## When you finish, report in this form
- What you did: each changed file and what changed, one line each
- What you checked: the commands you ran and their results ("empty", "12 of 12 passed")
- What you did NOT check: anything time or environment kept you from verifying
- What caught your eye: things you didn't fix but that seem worth a look
Enter fullscreen mode Exit fullscreen mode

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.

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.

Checklist

CLAUDE.md

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

Other agents' instruction files (AGENTS.md etc.)

  • Not made by find-and-replace; names and exclude lists make sense
  • The check has been fed a deliberately bad zip, once

settings.json

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

Hooks

  • A guard where something actually went wrong once
  • Normal work isn't blocked
  • A broken guard doesn't stop all work

Handovers

  • The handover document has a "what we decided not to do" chapter
  • The first message of a new session covers read, write it down, and start the next task

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.


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.

Top comments (0)